From fb888a673f7ff298bc14ca0f07b064d8dbf7697c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Wed, 29 Apr 2026 13:44:44 +0800 Subject: [PATCH] =?UTF-8?q?fix(analytics):=20=E7=B3=BB=E7=BB=9F=E6=80=A7?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Google=20Analytics=20=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E9=87=87=E9=9B=86=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复城市 (not set): 移除 allow_google_signals: false,启用 Google 信号补充地理数据 - 修复 Consent Mode v2: 补充 ad_user_data / ad_personalization 参数 - 修复 wait_for_update 与横幅延迟不匹配: 500ms → 3000ms - 修复 static export 兼容性: GA 初始化脚本从 client component 移至 layout.tsx head 原生 script 标签 - 修复 pageview 追踪: GA3 风格 gtag('config') → GA4 风格 gtag('event', 'page_view') - 修复 CookieConsent: 横幅延迟 2000ms → 500ms,同意后补发 pageview - 修复 PerformanceTracker: FID → INP (Core Web Vitals 2024 更新) - 修复环境变量命名: NEXT_PUBLIC_GA_ID → NEXT_PUBLIC_GA_MEASUREMENT_ID - 清理 deploy-dist.sh 冗余 server/app 分支逻辑 - 新增部署产物 GA 脚本嵌入验证 --- .env.example | 2 +- deploy-dist.sh | 26 +++-- deploy.sh | 2 +- src/app/layout.tsx | 42 ++++++++ src/components/analytics/CookieConsent.tsx | 8 +- src/components/analytics/GoogleAnalytics.tsx | 52 ++------- .../analytics/PerformanceTracker.tsx | 100 ++++++++++-------- src/lib/analytics.ts | 40 ++++--- 8 files changed, 157 insertions(+), 115 deletions(-) diff --git a/.env.example b/.env.example index a56f659..89c8dde 100644 --- a/.env.example +++ b/.env.example @@ -1 +1 @@ -NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX +NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX diff --git a/deploy-dist.sh b/deploy-dist.sh index 7b83f97..e030747 100755 --- a/deploy-dist.sh +++ b/deploy-dist.sh @@ -51,6 +51,20 @@ fi DIST_SIZE=$(du -sh "$DIST_DIR" | cut -f1) echo "✅ dist 目录大小: $DIST_SIZE" +echo "" +echo "📋 步骤1.1: 验证构建产物..." +if [ -f "$DIST_DIR/index.html" ]; then + if grep -q "googletagmanager.com" "$DIST_DIR/index.html"; then + GA_ID=$(grep -oP 'id=G-[A-Z0-9]+' "$DIST_DIR/index.html" | head -1 | sed 's/id=//') + echo "✅ GA 脚本已嵌入: $GA_ID" + else + echo "⚠️ 未检测到 GA 脚本,请检查 .env.production 中的 NEXT_PUBLIC_GA_MEASUREMENT_ID" + fi +else + echo "❌ index.html 不存在,构建可能失败" + exit 1 +fi + echo "" echo "📋 步骤2: 验证SSH连接..." if ! ssh -o ConnectTimeout=5 "$SERVER_USER@$SERVER_IP" exit; then @@ -79,17 +93,7 @@ echo "" echo "📋 步骤4: 上传 dist 目录..." ssh "$SERVER_USER@$SERVER_IP" "mkdir -p '$DEPLOY_ROOT/$STATIC_DIR'" -# 检查dist目录结构并正确处理Next.js静态文件 -if [ -d "$DIST_DIR/server/app" ]; then - echo "🔧 检测到Next.js静态导出结构,正在处理HTML文件..." - # 复制HTML文件到根目录 - rsync -avz --delete "$DIST_DIR/server/app/" "$SERVER_USER@$SERVER_IP:$DEPLOY_ROOT/$STATIC_DIR/" - # 复制其他静态资源 - rsync -avz --delete --exclude='server/app' "$DIST_DIR/" "$SERVER_USER@$SERVER_IP:$DEPLOY_ROOT/$STATIC_DIR/" -else - # 标准静态文件结构 - rsync -avz --delete "$DIST_DIR/" "$SERVER_USER@$SERVER_IP:$DEPLOY_ROOT/$STATIC_DIR/" -fi +rsync -avz --delete "$DIST_DIR/" "$SERVER_USER@$SERVER_IP:$DEPLOY_ROOT/$STATIC_DIR/" echo "✅ dist 目录已上传" diff --git a/deploy.sh b/deploy.sh index e1abd9e..15449a2 100755 --- a/deploy.sh +++ b/deploy.sh @@ -102,7 +102,7 @@ if [ ! -f .env ]; then echo "📝 创建.env文件..." cp .env.example .env echo "⚠️ 请编辑.env文件,填入正确的环境变量" - echo "⚠️ 可选配置: NEXT_PUBLIC_GA_ID" + echo "⚠️ 可选配置: NEXT_PUBLIC_GA_MEASUREMENT_ID" fi echo "🐳 启动Docker容器..." diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 319f8d0..e410ff8 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -15,6 +15,8 @@ import { ErrorBoundary } from "@/components/ui/error-boundary"; import { ScrollProgress } from "@/components/ui/scroll-progress"; import { BackToTop } from "@/components/ui/back-to-top"; +const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || ''; + const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"], @@ -127,6 +129,46 @@ export default function RootLayout({ + {GA_MEASUREMENT_ID && ( + <> + - - ); + return null; } export function GoogleAnalytics() { diff --git a/src/components/analytics/PerformanceTracker.tsx b/src/components/analytics/PerformanceTracker.tsx index 4f56f41..e06f14a 100644 --- a/src/components/analytics/PerformanceTracker.tsx +++ b/src/components/analytics/PerformanceTracker.tsx @@ -6,63 +6,69 @@ import { trackPerformance } from '@/lib/analytics'; export function PerformanceTracker() { useEffect(() => { if (typeof window === 'undefined') {return;} + if (!('PerformanceObserver' in window)) {return;} - const reportWebVitals = (): (() => void) | undefined => { - if ('PerformanceObserver' in window) { - const lcpObserver = new PerformanceObserver((list) => { - const entries = list.getEntries(); - const lastEntry = entries[entries.length - 1]; - if (lastEntry) { - trackPerformance('LCP', lastEntry.startTime); - } - }); + const observers: PerformanceObserver[] = []; - const fidObserver = new PerformanceObserver((list) => { - const entries = list.getEntries(); - const firstEntry = entries[0]; - if (firstEntry && 'processingStart' in firstEntry) { - const fidEntry = firstEntry as PerformanceEventTiming; - trackPerformance('FID', fidEntry.processingStart - fidEntry.startTime); - } - }); - - const clsObserver = new PerformanceObserver((list) => { - let clsValue = 0; - for (const entry of list.getEntries()) { - if ('value' in entry && !(entry as LayoutShift).hadRecentInput) { - clsValue += (entry as LayoutShift).value; - } - } - if (clsValue > 0) { - trackPerformance('CLS', clsValue * 1000); - } - }); - - try { - lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true }); - fidObserver.observe({ type: 'first-input', buffered: true }); - clsObserver.observe({ type: 'layout-shift', buffered: true }); - } catch { - // Observer not supported - } - - return () => { - lcpObserver.disconnect(); - fidObserver.disconnect(); - clsObserver.disconnect(); - }; + const lcpObserver = new PerformanceObserver((list) => { + const entries = list.getEntries(); + const lastEntry = entries[entries.length - 1]; + if (lastEntry) { + trackPerformance('LCP', lastEntry.startTime); } - return undefined; - }; + }); - const cleanup = reportWebVitals(); - return cleanup; + const inpObserver = new PerformanceObserver((list) => { + const entries = list.getEntries(); + const lastEntry = entries[entries.length - 1]; + if (lastEntry && 'duration' in lastEntry) { + trackPerformance('INP', (lastEntry as PerformanceEventTiming).duration); + } + }); + + const clsObserver = new PerformanceObserver((list) => { + let clsValue = 0; + for (const entry of list.getEntries()) { + if ('value' in entry && !(entry as LayoutShift).hadRecentInput) { + clsValue += (entry as LayoutShift).value; + } + } + if (clsValue > 0) { + trackPerformance('CLS', clsValue * 1000); + } + }); + + try { + lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true }); + observers.push(lcpObserver); + } catch { + // LCP observer not supported + } + + try { + inpObserver.observe({ type: 'event', buffered: true }); + observers.push(inpObserver); + } catch { + // INP observer not supported + } + + try { + clsObserver.observe({ type: 'layout-shift', buffered: true }); + observers.push(clsObserver); + } catch { + // CLS observer not supported + } + + return () => { + observers.forEach((o) => o.disconnect()); + }; }, []); return null; } interface PerformanceEventTiming extends PerformanceEntry { + duration: number; processingStart: number; startTime: number; } diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index 2709eaa..ffca557 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -10,14 +10,6 @@ declare global { } } -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, { @@ -28,6 +20,30 @@ export const event = (action: string, category: string, label?: string, value?: } }; +export const hasAnalyticsConsent = (): boolean => { + if (typeof window === 'undefined') {return false;} + try { + const stored = localStorage.getItem('cookie_preferences'); + if (stored) { + const prefs = JSON.parse(stored) as CookiePreferences; + return prefs.analytics === true; + } + } catch { + return false; + } + return false; +}; + +export const trackPageView = (pageTitle: string, pagePath: string) => { + if (typeof window !== 'undefined' && window.gtag && GA_MEASUREMENT_ID) { + window.gtag('event', 'page_view', { + page_title: pageTitle, + page_location: window.location.origin + pagePath, + page_path: pagePath, + }); + } +}; + export const trackContactForm = (formData: Record) => { event('generate_lead', 'engagement', 'contact_form_submission'); @@ -44,10 +60,6 @@ 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', { @@ -161,6 +173,8 @@ export const updateConsent = (granted: boolean) => { window.gtag('consent', 'update', { analytics_storage: granted ? 'granted' : 'denied', ad_storage: 'denied', + ad_user_data: 'denied', + ad_personalization: 'denied', }); } }; @@ -170,6 +184,8 @@ export const updateConsentDetailed = (preferences: CookiePreferences) => { window.gtag('consent', 'update', { analytics_storage: preferences.analytics ? 'granted' : 'denied', ad_storage: preferences.marketing ? 'granted' : 'denied', + ad_user_data: preferences.marketing ? 'granted' : 'denied', + ad_personalization: preferences.marketing ? 'granted' : 'denied', functionality_storage: 'granted', personalization_storage: preferences.marketing ? 'granted' : 'denied', security_storage: 'granted',