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:
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user