fix(analytics): 系统性修复 Google Analytics 数据采集问题

- 修复城市 (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 脚本嵌入验证
This commit is contained in:
张翔
2026-04-29 13:44:44 +08:00
parent 5d14a0780c
commit fb888a673f
8 changed files with 157 additions and 115 deletions
+7 -1
View File
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from 'react';
import {
updateConsentDetailed,
trackButtonClick,
trackPageView,
CookiePreferences,
getStoredPreferences,
storePreferences,
@@ -38,7 +39,7 @@ export function CookieConsent() {
} else {
const timer = setTimeout(() => {
setShowConsent(true);
}, 2000);
}, 500);
return () => clearTimeout(timer);
}
}
@@ -51,6 +52,11 @@ export function CookieConsent() {
storePreferences(finalPrefs);
updateConsentDetailed(finalPrefs);
trackButtonClick('save_cookie_preferences', 'consent_banner');
if (prefs.analytics) {
setTimeout(() => {
trackPageView(document.title, window.location.pathname);
}, 100);
}
setTimeout(() => {
setShowConsent(false);
setShowSettings(false);
+10 -42
View File
@@ -1,8 +1,8 @@
'use client';
import Script from 'next/script';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect, Suspense } from 'react';
import { hasAnalyticsConsent } from '@/lib/analytics';
const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || '';
@@ -12,50 +12,18 @@ function GoogleAnalyticsContent() {
useEffect(() => {
if (!GA_MEASUREMENT_ID || typeof window === 'undefined') {return;}
if (!hasAnalyticsConsent()) {return;}
const url = pathname + (searchParams.toString() ? `?${searchParams.toString()}` : '');
if (window.gtag) {
window.gtag('config', GA_MEASUREMENT_ID, {
page_path: url,
page_title: document.title,
page_location: window.location.origin + url,
});
}
window.gtag('event', 'page_view', {
page_title: document.title,
page_location: window.location.origin + url,
page_path: url,
});
}, [pathname, searchParams]);
if (!GA_MEASUREMENT_ID) {return null;}
return (
<>
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
strategy="afterInteractive"
/>
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
// 默认禁用存储,等待用户同意
gtag('consent', 'default', {
'analytics_storage': 'denied',
'ad_storage': 'denied',
'wait_for_update': 500
});
gtag('config', '${GA_MEASUREMENT_ID}', {
send_page_view: false,
anonymize_ip: true,
allow_google_signals: false,
allow_ad_personalization_signals: false,
cookie_flags: 'SameSite=None;Secure'
});
`}
</Script>
</>
);
return null;
}
export function GoogleAnalytics() {
+53 -47
View File
@@ -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;
}