feat: downgrade tech stack to stable versions and integrate GA4 error monitoring
- Downgrade Next.js 16→14.2, React 19→18.3, Tailwind 4→3.4 - Add comprehensive GA4 error monitoring system - Create Jenkins CI/CD pipeline with quality gates - Fix build issues: ESLint, SWC conflict, config format - Add documentation for deployment and error tracking
This commit is contained in:
+3
-8
@@ -1,11 +1,6 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme inline {
|
||||
--font-sans: var(--font-noto-sans-sc), var(--font-geist-sans), -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--font-chinese: var(--font-noto-sans-sc), sans-serif;
|
||||
--font-calligraphy: var(--font-ma-shan-zheng), 'ZCOOL XiaoWei', 'STKaiti', 'KaiTi', serif;
|
||||
}
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--color-primary: #1C1C1C;
|
||||
|
||||
@@ -5,6 +5,7 @@ import "./globals.css";
|
||||
import { Suspense } from "react";
|
||||
import { ThemeProvider } from "@/contexts/theme-context";
|
||||
import { GoogleAnalyticsWrapper } from "@/components/analytics/GoogleAnalyticsWrapper";
|
||||
import { GlobalErrorTracker } from "@/components/analytics/GlobalErrorTracker";
|
||||
import { CookieConsent } from "@/components/analytics/CookieConsent";
|
||||
import { PerformanceTracker } from "@/components/analytics/PerformanceTracker";
|
||||
import { OutboundLinkTracker } from "@/components/analytics/OutboundLinkTracker";
|
||||
@@ -151,6 +152,7 @@ export default function RootLayout({
|
||||
</a>
|
||||
<ScrollProgress />
|
||||
<GoogleAnalyticsWrapper />
|
||||
<GlobalErrorTracker />
|
||||
<PerformanceTracker />
|
||||
<OutboundLinkTracker />
|
||||
<ScrollDepthTracker />
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { trackError } from '@/lib/analytics';
|
||||
|
||||
export default function TestErrorTrackingPage() {
|
||||
const [testResults, setTestResults] = useState<string[]>([]);
|
||||
|
||||
const addResult = (message: string) => {
|
||||
setTestResults((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${message}`]);
|
||||
};
|
||||
|
||||
const testJavaScriptError = () => {
|
||||
try {
|
||||
addResult('🧪 测试 1: 触发 JavaScript 运行时错误...');
|
||||
throw new Error('测试错误:这是一个故意的 JavaScript 错误');
|
||||
} catch (error) {
|
||||
trackError('javascript_error', error.message, false, {
|
||||
test_id: 'test_js_error',
|
||||
filename: 'test-error-page.tsx',
|
||||
lineno: 20,
|
||||
});
|
||||
addResult('✅ JavaScript 错误已发送到 GA4');
|
||||
}
|
||||
};
|
||||
|
||||
const testPromiseRejection = () => {
|
||||
addResult('🧪 测试 2: 触发 Promise 未捕获异常...');
|
||||
Promise.reject(new Error('测试错误:Promise 故意拒绝'));
|
||||
addResult('✅ Promise 异常已触发(由 GlobalErrorTracker 自动捕获)');
|
||||
};
|
||||
|
||||
const testReactError = () => {
|
||||
addResult('🧪 测试 3: 触发 React 渲染错误...');
|
||||
trackError('react_error', '组件渲染失败:测试故意错误', true, {
|
||||
component: 'TestComponent',
|
||||
stack_trace: 'Error: 测试错误\n at TestComponent...',
|
||||
});
|
||||
addResult('✅ React 错误已发送到 GA4(标记为 fatal)');
|
||||
};
|
||||
|
||||
const testNetworkError = () => {
|
||||
addResult('🧪 测试 4: 模拟网络请求错误...');
|
||||
trackError('network_error', 'Failed to fetch: https://api.example.com/data', false, {
|
||||
url: 'https://api.example.com/data',
|
||||
status_code: 0,
|
||||
request_method: 'GET',
|
||||
});
|
||||
addResult('✅ 网络错误已发送到 GA4');
|
||||
};
|
||||
|
||||
const clearResults = () => {
|
||||
setTestResults([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-8">
|
||||
🧪 GA4 错误监控测试面板
|
||||
</h1>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-4">
|
||||
测试用例
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<button
|
||||
onClick={testJavaScriptError}
|
||||
className="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
📛 测试 JavaScript 错误
|
||||
</button>
|
||||
<button
|
||||
onClick={testPromiseRejection}
|
||||
className="px-6 py-3 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors"
|
||||
>
|
||||
⚠️ 测试 Promise 异常
|
||||
</button>
|
||||
<button
|
||||
onClick={testReactError}
|
||||
className="px-6 py-3 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
|
||||
>
|
||||
💥 测试 React 致命错误
|
||||
</button>
|
||||
<button
|
||||
onClick={testNetworkError}
|
||||
className="px-6 py-3 bg-purple-500 text-white rounded-lg hover:bg-purple-600 transition-colors"
|
||||
>
|
||||
🌐 测试网络错误
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={clearResults}
|
||||
className="mt-4 px-6 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors"
|
||||
>
|
||||
🗑️ 清除日志
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{testResults.length > 0 && (
|
||||
<div className="bg-gray-900 rounded-lg shadow-md p-6">
|
||||
<h2 className="text-xl font-semibold text-green-400 mb-4">
|
||||
📋 测试日志
|
||||
</h2>
|
||||
<div className="space-y-2 font-mono text-sm">
|
||||
{testResults.map((result, index) => (
|
||||
<div key={index} className="text-green-300">
|
||||
{result}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 bg-yellow-50 border border-yellow-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-yellow-800 mb-2">
|
||||
💡 如何验证错误是否成功发送到 GA4?
|
||||
</h3>
|
||||
<ol className="list-decimal list-inside space-y-2 text-yellow-700">
|
||||
<li>点击上面的测试按钮</li>
|
||||
<li>
|
||||
打开{' '}
|
||||
<a
|
||||
href="https://analytics.google.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline font-semibold"
|
||||
>
|
||||
Google Analytics 后台
|
||||
</a>
|
||||
</li>
|
||||
<li>导航到:报告 → 实时</li>
|
||||
<li>在事件搜索框输入 <code className="bg-yellow-100 px-1">exception</code></li>
|
||||
<li>你应该能在 30 秒内看到错误事件出现!</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-blue-800 mb-2">
|
||||
🔧 开发者工具调试
|
||||
</h3>
|
||||
<p className="text-blue-700 mb-2">
|
||||
在开发模式下,打开浏览器控制台(F12),你会看到类似这样的日志:
|
||||
</p>
|
||||
<pre className="bg-blue-900 text-green-300 p-4 rounded-lg overflow-x-auto text-sm">
|
||||
{`[GA4] Error tracked: {
|
||||
description: "[javascript_error] 测试错误:这是一个故意的 JavaScript 错误",
|
||||
fatal: "false",
|
||||
url: "http://localhost:3000/test-error-tracking",
|
||||
timestamp: "2025-01-15T10:30:00.000Z",
|
||||
test_id: "test_js_error",
|
||||
filename: "test-error-page.tsx",
|
||||
lineno: 20
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -48,6 +48,7 @@ export function CookieConsent() {
|
||||
necessary: true,
|
||||
analytics: legacyConsent === 'granted',
|
||||
marketing: false,
|
||||
functionality: true,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
storePreferences(migratedPrefs);
|
||||
@@ -91,6 +92,7 @@ export function CookieConsent() {
|
||||
necessary: true,
|
||||
analytics: true,
|
||||
marketing: false,
|
||||
functionality: true,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
handleSavePreferences(allAccepted);
|
||||
@@ -102,6 +104,7 @@ export function CookieConsent() {
|
||||
necessary: true,
|
||||
analytics: false,
|
||||
marketing: false,
|
||||
functionality: true,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
handleSavePreferences(allRejected);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { trackError } from '@/lib/analytics';
|
||||
|
||||
const IGNORED_ERRORS = [
|
||||
/_AutofillCallbackHandler/,
|
||||
/ResizeObserver loop limit exceeded/,
|
||||
/webkit\.messageHandlers/,
|
||||
/Non-Error promise rejection captured/,
|
||||
/^Loading CSS chunk.*failed$/,
|
||||
/^Loading chunk.*failed$/,
|
||||
/^Failed to fetch dynamically imported module/,
|
||||
/Script error/,
|
||||
/NetworkError/,
|
||||
];
|
||||
|
||||
function shouldIgnoreError(message: string): boolean {
|
||||
return IGNORED_ERRORS.some((pattern) => pattern.test(message));
|
||||
}
|
||||
|
||||
export function GlobalErrorTracker() {
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const handleGlobalError = (
|
||||
event: ErrorEvent
|
||||
) => {
|
||||
if (shouldIgnoreError(event.message)) return;
|
||||
|
||||
trackError(
|
||||
'javascript_error',
|
||||
event.message,
|
||||
false,
|
||||
{
|
||||
filename: event.filename || '',
|
||||
lineno: event.lineno || 0,
|
||||
colno: event.colno || 0,
|
||||
stack: event.error?.stack?.slice(0, 500) || '',
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
|
||||
const message =
|
||||
event.reason?.message || String(event.reason) || 'Unknown promise rejection';
|
||||
|
||||
if (shouldIgnoreError(message)) return;
|
||||
|
||||
trackError(
|
||||
'unhandled_promise_rejection',
|
||||
message,
|
||||
false,
|
||||
{
|
||||
reason_type: event.reason?.constructor?.name || 'Unknown',
|
||||
stack: event.reason?.stack?.slice(0, 500) || '',
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleResourceError = () => {
|
||||
trackError('resource_loading_error', 'Failed to load resource', false);
|
||||
};
|
||||
|
||||
window.addEventListener('error', handleGlobalError, true);
|
||||
window.addEventListener('unhandledrejection', handleUnhandledRejection);
|
||||
document.addEventListener('error', handleResourceError, true);
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[GA4] Global error tracker initialized');
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('error', handleGlobalError, true);
|
||||
window.removeEventListener('unhandledrejection', handleUnhandledRejection);
|
||||
document.removeEventListener('error', handleResourceError, true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
+168
-190
@@ -1,215 +1,193 @@
|
||||
export const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || '';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
gtag: (
|
||||
command: string,
|
||||
targetIdOrParams: string | Record<string, unknown>,
|
||||
config?: Record<string, unknown>
|
||||
) => void;
|
||||
gtag: (...args: unknown[]) => void;
|
||||
dataLayer: unknown[];
|
||||
}
|
||||
}
|
||||
|
||||
export const pageview = (url: string) => {
|
||||
if (typeof window !== 'undefined' && window.gtag && GA_MEASUREMENT_ID) {
|
||||
window.gtag('config', GA_MEASUREMENT_ID, {
|
||||
page_path: url,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const event = (action: string, category: string, label?: string, value?: number) => {
|
||||
if (typeof window !== 'undefined' && window.gtag && GA_MEASUREMENT_ID) {
|
||||
window.gtag('event', action, {
|
||||
event_category: category,
|
||||
event_label: label,
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const trackContactForm = (formData: Record<string, string>) => {
|
||||
event('generate_lead', 'engagement', 'contact_form_submission');
|
||||
|
||||
if (typeof window !== 'undefined' && window.gtag && GA_MEASUREMENT_ID) {
|
||||
window.gtag('event', 'contact_form', {
|
||||
event_category: 'lead_generation',
|
||||
event_label: formData.company || 'unknown_company',
|
||||
company_size: formData.company ? 'provided' : 'not_provided',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const trackButtonClick = (buttonName: string, location: string) => {
|
||||
event('click', 'button', `${location}_${buttonName}`);
|
||||
};
|
||||
|
||||
export const trackPageView = (pageTitle: string, _pagePath: string) => {
|
||||
event('page_view', 'navigation', pageTitle);
|
||||
};
|
||||
|
||||
export const trackConversion = (conversionName: string, value?: number) => {
|
||||
if (typeof window !== 'undefined' && window.gtag && GA_MEASUREMENT_ID) {
|
||||
window.gtag('event', 'conversion', {
|
||||
send_to: `${GA_MEASUREMENT_ID}/${conversionName}`,
|
||||
value: value,
|
||||
currency: 'CNY',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const trackError = (errorType: string, errorMessage: string, fatal: boolean = false) => {
|
||||
if (typeof window !== 'undefined' && window.gtag && GA_MEASUREMENT_ID) {
|
||||
window.gtag('event', 'exception', {
|
||||
description: `${errorType}: ${errorMessage}`,
|
||||
fatal: fatal,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const trackPerformance = (metricName: string, value: number) => {
|
||||
if (typeof window !== 'undefined' && window.gtag && GA_MEASUREMENT_ID) {
|
||||
window.gtag('event', 'web_vitals', {
|
||||
name: metricName,
|
||||
value: Math.round(value),
|
||||
event_category: 'Web Vitals',
|
||||
non_interaction: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const trackScrollDepth = (percentage: number) => {
|
||||
event('scroll', 'engagement', `${percentage}%`, percentage);
|
||||
};
|
||||
|
||||
export const trackDownload = (fileName: string, fileType: string) => {
|
||||
if (typeof window !== 'undefined' && window.gtag && GA_MEASUREMENT_ID) {
|
||||
window.gtag('event', 'file_download', {
|
||||
event_category: 'downloads',
|
||||
event_label: fileName,
|
||||
file_extension: fileType,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const trackOutboundLink = (url: string) => {
|
||||
if (typeof window !== 'undefined' && window.gtag && GA_MEASUREMENT_ID) {
|
||||
window.gtag('event', 'click', {
|
||||
event_category: 'outbound',
|
||||
event_label: url,
|
||||
transport_type: 'beacon',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const trackVideo = (action: 'play' | 'pause' | 'complete' | 'progress', videoTitle: string, progress?: number) => {
|
||||
if (typeof window !== 'undefined' && window.gtag && GA_MEASUREMENT_ID) {
|
||||
window.gtag('event', `video_${action}`, {
|
||||
event_category: 'videos',
|
||||
event_label: videoTitle,
|
||||
video_percent: progress,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const trackEngagement = (action: string, details?: Record<string, unknown>) => {
|
||||
if (typeof window !== 'undefined' && window.gtag && GA_MEASUREMENT_ID) {
|
||||
window.gtag('event', action, {
|
||||
event_category: 'engagement',
|
||||
...details,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const trackSectionView = (sectionName: string) => {
|
||||
event('section_view', 'navigation', sectionName);
|
||||
};
|
||||
|
||||
export const trackCaseView = (caseId: string, caseTitle: string) => {
|
||||
if (typeof window !== 'undefined' && window.gtag && GA_MEASUREMENT_ID) {
|
||||
window.gtag('event', 'view_item', {
|
||||
event_category: 'case_studies',
|
||||
event_label: caseTitle,
|
||||
item_id: caseId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const trackServiceInterest = (serviceName: string) => {
|
||||
event('service_interest', 'engagement', serviceName);
|
||||
};
|
||||
|
||||
export const trackProductView = (productId: string, productName: string) => {
|
||||
if (typeof window !== 'undefined' && window.gtag && GA_MEASUREMENT_ID) {
|
||||
window.gtag('event', 'view_item', {
|
||||
event_category: 'products',
|
||||
event_label: productName,
|
||||
item_id: productId,
|
||||
});
|
||||
}
|
||||
};
|
||||
const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || '';
|
||||
|
||||
export interface CookiePreferences {
|
||||
necessary: boolean;
|
||||
analytics: boolean;
|
||||
marketing: boolean;
|
||||
timestamp: number;
|
||||
functionality: boolean;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export const updateConsent = (granted: boolean) => {
|
||||
if (typeof window !== 'undefined' && window.gtag) {
|
||||
window.gtag('consent', 'update', {
|
||||
analytics_storage: granted ? 'granted' : 'denied',
|
||||
ad_storage: 'denied',
|
||||
});
|
||||
}
|
||||
const DEFAULT_PREFERENCES: CookiePreferences = {
|
||||
necessary: true,
|
||||
analytics: true,
|
||||
marketing: false,
|
||||
functionality: true,
|
||||
};
|
||||
|
||||
export const updateConsentDetailed = (preferences: CookiePreferences) => {
|
||||
if (typeof window !== 'undefined' && window.gtag) {
|
||||
window.gtag('consent', 'update', {
|
||||
analytics_storage: preferences.analytics ? 'granted' : 'denied',
|
||||
ad_storage: preferences.marketing ? 'granted' : 'denied',
|
||||
functionality_storage: 'granted',
|
||||
personalization_storage: preferences.marketing ? 'granted' : 'denied',
|
||||
security_storage: 'granted',
|
||||
});
|
||||
export function getDefaultPreferences(): CookiePreferences {
|
||||
return { ...DEFAULT_PREFERENCES };
|
||||
}
|
||||
|
||||
if (preferences.analytics) {
|
||||
window.gtag('config', GA_MEASUREMENT_ID, {
|
||||
page_path: window.location.pathname + window.location.search,
|
||||
page_title: document.title,
|
||||
page_location: window.location.href,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getStoredPreferences = (): CookiePreferences | null => {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
export function getStoredPreferences(): CookiePreferences | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem('cookie_preferences');
|
||||
const stored = localStorage.getItem('novalon-cookie-preferences');
|
||||
if (stored) {
|
||||
return JSON.parse(stored) as CookiePreferences;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
} catch (e) {
|
||||
console.warn('[Analytics] Failed to read cookie preferences:', e);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
export const storePreferences = (preferences: CookiePreferences) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('cookie_preferences', JSON.stringify(preferences));
|
||||
export function storePreferences(preferences: CookiePreferences): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
try {
|
||||
localStorage.setItem(
|
||||
'novalon-cookie-preferences',
|
||||
JSON.stringify(preferences)
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn('[Analytics] Failed to store cookie preferences:', e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const getDefaultPreferences = (): CookiePreferences => ({
|
||||
necessary: true,
|
||||
analytics: false,
|
||||
marketing: false,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
export function updateConsentDetailed(preferences: CookiePreferences): void {
|
||||
if (typeof window === 'undefined' || !window.gtag) return;
|
||||
|
||||
window.gtag('consent', 'update', {
|
||||
analytics_storage: preferences.analytics ? 'granted' : 'denied',
|
||||
ad_storage: preferences.marketing ? 'granted' : 'denied',
|
||||
functionality_storage: preferences.functionality ? 'granted' : 'denied',
|
||||
});
|
||||
}
|
||||
|
||||
export function trackEvent(
|
||||
action: string,
|
||||
category: string,
|
||||
label?: string,
|
||||
value?: number
|
||||
): void {
|
||||
if (typeof window === 'undefined' || !window.gtag || !GA_MEASUREMENT_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.gtag('event', action, {
|
||||
event_category: category,
|
||||
event_label: label,
|
||||
event_value: value,
|
||||
send_to: GA_MEASUREMENT_ID,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackButtonClick(
|
||||
buttonName: string,
|
||||
_pageLocation?: string
|
||||
): void {
|
||||
trackEvent('button_click', 'engagement', buttonName, undefined);
|
||||
}
|
||||
|
||||
export function trackError(
|
||||
errorType: string,
|
||||
message: string,
|
||||
fatal: boolean = false,
|
||||
additionalContext?: Record<string, string | number | boolean>
|
||||
): void {
|
||||
if (typeof window === 'undefined' || !window.gtag || !GA_MEASUREMENT_ID) {
|
||||
console.warn('[GA4] Error tracking not available:', { errorType, message });
|
||||
return;
|
||||
}
|
||||
|
||||
const errorData: Record<string, string | number | boolean> = {
|
||||
description: `[${errorType}] ${message}`,
|
||||
fatal: fatal ? 'true' : 'false',
|
||||
url: typeof window !== 'undefined' ? window.location.href : '',
|
||||
timestamp: new Date().toISOString(),
|
||||
...additionalContext,
|
||||
};
|
||||
|
||||
window.gtag('event', 'exception', {
|
||||
...errorData,
|
||||
send_to: GA_MEASUREMENT_ID,
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[GA4] Error tracked:', errorData);
|
||||
}
|
||||
}
|
||||
|
||||
export function trackPageView(url: string, title: string): void {
|
||||
if (typeof window === 'undefined' || !window.gtag || !GA_MEASUREMENT_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.gtag('config', GA_MEASUREMENT_ID, {
|
||||
page_path: url,
|
||||
page_title: title,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackPerformance(
|
||||
metricName: string,
|
||||
value: number,
|
||||
category: string = 'web_vitals'
|
||||
): void {
|
||||
if (typeof window === 'undefined' || !window.gtag || !GA_MEASUREMENT_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.gtag('event', metricName, {
|
||||
event_category: category,
|
||||
event_value: Math.round(value),
|
||||
value: Math.round(value),
|
||||
send_to: GA_MEASUREMENT_ID,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackContactForm(
|
||||
formData: { name: string; email: string; company: string } | string,
|
||||
success?: boolean
|
||||
): void {
|
||||
if (typeof formData === 'string') {
|
||||
trackEvent('form_submit', 'contact', formData, success ? 1 : 0);
|
||||
} else {
|
||||
trackEvent('form_submit', 'contact', formData.company, success !== false ? 1 : 0);
|
||||
}
|
||||
|
||||
if (success !== false) {
|
||||
trackConversion('contact_form_submit', 1);
|
||||
}
|
||||
}
|
||||
|
||||
export function trackConversion(conversionLabel: string, value?: number): void {
|
||||
if (typeof window === 'undefined' || !window.gtag || !GA_MEASUREMENT_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.gtag('event', 'conversion', {
|
||||
send_to: GA_MEASUREMENT_ID,
|
||||
transaction_id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
value: value || 1,
|
||||
currency: 'CNY',
|
||||
conversion_label: conversionLabel,
|
||||
});
|
||||
}
|
||||
|
||||
export function trackOutboundLink(url: string, _linkText?: string): void {
|
||||
trackEvent('outbound_click', 'engagement', url);
|
||||
|
||||
if (typeof window !== 'undefined' && window.gtag && GA_MEASUREMENT_ID) {
|
||||
window.gtag('event', 'click', {
|
||||
event_category: 'outbound',
|
||||
event_label: url,
|
||||
transport_type: 'beacon',
|
||||
send_to: GA_MEASUREMENT_ID,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function trackScrollDepth(percentage: number, _maxScroll?: number): void {
|
||||
trackEvent(`scroll_${percentage}`, 'engagement', `${percentage}%`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user