feat(analytics): enhance Google Analytics with privacy compliance and comprehensive tracking

- Add automatic route change tracking for SPA navigation
- Implement Cookie consent banner for GDPR compliance
- Add performance tracking (LCP, FID, CLS Web Vitals)
- Add outbound link click tracking
- Integrate contact form submission tracking with conversion events
- Add CTA button click tracking in hero section
- Integrate error tracking in ErrorBoundary component
- Extend analytics utility library with 15+ tracking functions
- Configure IP anonymization and privacy settings
- Remove unused test files and deployment scripts
- Update case studies to include only specified cases
- Fix mobile navigation active state issues
- Fix lint errors in test files and components

BREAKING CHANGE: Google Analytics now requires user consent before tracking
This commit is contained in:
张翔
2026-04-22 07:19:29 +08:00
parent b117372b03
commit 2f45818724
45 changed files with 652 additions and 2293 deletions
+125 -3
View File
@@ -2,7 +2,11 @@ export const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || ''
declare global {
interface Window {
gtag: (command: string, targetId: string, config?: Record<string, unknown>) => void;
gtag: (
command: string,
targetIdOrParams: string | Record<string, unknown>,
config?: Record<string, unknown>
) => void;
}
}
@@ -24,8 +28,16 @@ export const event = (action: string, category: string, label?: string, value?:
}
};
export const trackContactForm = (_formData: Record<string, string>) => {
event('submit', 'contact_form', 'contact_form_submission');
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) => {
@@ -35,3 +47,113 @@ export const trackButtonClick = (buttonName: string, location: string) => {
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,
});
}
};
export const updateConsent = (granted: boolean) => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('consent', 'update', {
analytics_storage: granted ? 'granted' : 'denied',
ad_storage: 'denied',
});
}
};
+8 -8
View File
@@ -113,10 +113,10 @@ describe('Constants', () => {
expect(softwareService?.title).toBe('软件开发');
});
it('should have cloud service', () => {
const cloudService = SERVICES.find(s => s.id === 'cloud');
expect(cloudService).toBeDefined();
expect(cloudService?.title).toBe('云服务');
it('should have consulting service', () => {
const consultingService = SERVICES.find(s => s.id === 'consulting');
expect(consultingService).toBeDefined();
expect(consultingService?.title).toBe('技术咨询');
});
it('should have data service', () => {
@@ -125,10 +125,10 @@ describe('Constants', () => {
expect(dataService?.title).toBe('数据分析');
});
it('should have security service', () => {
const securityService = SERVICES.find(s => s.id === 'security');
expect(securityService).toBeDefined();
expect(securityService?.title).toBe('信息安全');
it('should have solutions service', () => {
const solutionsService = SERVICES.find(s => s.id === 'solutions');
expect(solutionsService).toBeDefined();
expect(solutionsService?.title).toBe('解决方案');
});
it('should have features as array', () => {
+1 -1
View File
@@ -29,7 +29,7 @@ export interface CaseItem {
/** 成果数据 */
results: CaseResult[];
/** 客户证言 */
testimonial: CaseTestimonial;
testimonial?: CaseTestimonial;
tags: string[];
image: string;
/** 合作时长 */
+3 -74
View File
@@ -1,4 +1,4 @@
export type NewsCategory = '公司新闻' | '产品发布' | '合作动态' | '行业资讯';
export type NewsCategory = '公司新闻' | '产品发布';
export interface NewsItem {
id: string;
@@ -17,7 +17,7 @@ export const NEWS: NewsItem[] = [
excerpt: '2026年1月15日,四川睿新致远科技有限公司在成都龙泉驿区正式成立,标志着公司在科技创新领域迈出了坚实的第一步。',
date: '2026-01-15',
category: '公司新闻',
image: '/images/news/founding.jpg',
image: '/images/news/founding.png',
content: `2026年1月15日,四川睿新致远科技有限公司在成都龙泉驿区幸福路12号正式成立。公司注册资本雄厚,拥有一支经验丰富的技术团队。
公司专注于信息技术服务与解决方案,致力于为企业提供全方位的数字化转型支持。成立之初,公司就确立了"专注科技创新,驱动智慧未来"的企业使命。
@@ -32,7 +32,7 @@ export const NEWS: NewsItem[] = [
excerpt: '针对中小企业数字化转型需求,公司推出一站式数字化转型解决方案,帮助企业快速实现数字化升级。',
date: '2026-01-20',
category: '产品发布',
image: '/images/news/solution.jpg',
image: '/images/news/solution.png',
content: `近日,四川睿新致远科技有限公司正式推出企业数字化转型解决方案,该方案整合了云计算、大数据、人工智能等前沿技术,为中小企业提供一站式的数字化升级服务。
该解决方案包括:
@@ -45,75 +45,4 @@ export const NEWS: NewsItem[] = [
目前,该解决方案已在多个行业成功落地,获得了客户的一致好评。`,
},
{
id: '3',
title: '与本地制造企业达成战略合作协议',
excerpt: '公司与成都某知名制造企业签署战略合作协议,双方将共同打造智能制造示范工厂。',
date: '2026-01-25',
category: '合作动态',
image: '/images/news/partnership.jpg',
content: `1月25日,四川睿新致远科技有限公司与成都某知名制造企业正式签署战略合作协议。根据协议,双方将在智能制造、工业互联网、数字化转型等领域展开深度合作。
此次合作的主要内容包括:
- 建设智能制造示范工厂
- 开发工业互联网平台
- 实施生产数字化管理系统
- 开展技术人才培训
该制造企业负责人表示:"选择睿新致远作为合作伙伴,是看中了他们在数字化转型领域的专业能力和丰富经验。我们相信,通过双方的紧密合作,一定能够打造出行业领先的智能制造标杆。"
公司项目团队已进驻现场,开始前期调研和方案设计工作。`,
},
{
id: '4',
title: '公司加入四川省软件行业协会',
excerpt: '公司正式加入四川省软件行业协会,将积极参与行业交流与合作,推动本地软件产业发展。',
date: '2026-02-01',
category: '公司新闻',
image: '/images/news/membership.jpg',
content: `2月1日,四川睿新致远科技有限公司正式加入四川省软件行业协会,成为协会成员单位。这标志着公司在软件行业的专业地位得到了行业认可。
四川省软件行业协会是省内软件行业最具权威性的行业组织,拥有会员单位数百家。加入协会后,公司将享有以下权益:
- 参与行业标准制定
- 获取政策信息和行业动态
- 参加行业培训和交流活动
- 享受会员专属服务
公司表示,将积极参与协会组织的各项活动,与行业同仁加强交流合作,共同推动四川省软件产业高质量发展。同时,公司也将严格遵守行业规范,坚持诚信经营,为客户提供优质的产品和服务。`,
},
{
id: '5',
title: '2026年企业数字化转型趋势报告发布',
excerpt: '公司发布《2026年企业数字化转型趋势报告》,深入分析行业发展趋势,为企业提供转型参考。',
date: '2026-02-02',
category: '行业资讯',
image: '/images/news/report.jpg',
content: `四川睿新致远科技有限公司今日发布《2026年企业数字化转型趋势报告》,该报告基于对数百家企业的调研分析,深入剖析了当前企业数字化转型的现状、挑战与机遇。
报告主要发现:
1. 数字化转型已成为企业共识
调研显示,超过85%的企业已将数字化转型列为战略优先级,较2025年提升15个百分点。
2. 中小企业转型需求迫切`,
},
{
id: '6',
title: '公司获得ISO9001质量管理体系认证',
excerpt: '经过严格审核,公司正式获得ISO9001质量管理体系认证,标志着公司质量管理水平迈上新台阶。',
date: '2026-02-10',
category: '公司新闻',
image: '/images/news/iso9001.jpg',
content: `2月10日,四川睿新致远科技有限公司正式获得ISO9001质量管理体系认证证书。这是公司在质量管理领域取得的重要里程碑。
ISO9001认证是国际公认的质量管理体系标准,获得该认证意味着公司在以下方面达到了国际标准:
- 客户需求识别和满足能力
- 产品和服务质量控制能力
- 持续改进机制
- 风险管理能力
公司质量负责人表示:"获得ISO9001认证是对我们质量管理工作的肯定,也是新的起点。我们将继续坚持'质量第一'的原则,不断提升产品和服务质量,为客户创造更大价值。"
该认证的获得,将有助于公司进一步提升市场竞争力,赢得更多客户的信任。`,
},
] as const;
-80
View File
@@ -1,80 +0,0 @@
import { sanitizeHTML, sanitizeInput, sanitizeURL, escapeHTML } from './sanitize';
describe('sanitize', () => {
describe('sanitizeHTML', () => {
it('should allow safe HTML tags', () => {
const result = sanitizeHTML('<p>Hello <b>world</b></p>');
expect(result).toContain('<p>');
expect(result).toContain('<b>');
});
it('should remove dangerous tags', () => {
const result = sanitizeHTML('<script>alert("xss")</script><p>safe</p>');
expect(result).not.toContain('<script>');
expect(result).toContain('<p>');
});
it('should remove dangerous attributes', () => {
const result = sanitizeHTML('<a href="#" onclick="alert(1)">link</a>');
expect(result).not.toContain('onclick');
});
it('should handle empty input', () => {
expect(sanitizeHTML('')).toBe('');
});
});
describe('sanitizeInput', () => {
it('should remove all HTML tags', () => {
const result = sanitizeInput('<p>Hello <b>world</b></p>');
expect(result).not.toContain('<p>');
expect(result).not.toContain('<b>');
expect(result).toContain('Hello');
expect(result).toContain('world');
});
it('should handle special characters', () => {
const result = sanitizeInput('<script>alert("xss")</script>');
expect(result).not.toContain('<script>');
});
});
describe('sanitizeURL', () => {
it('should allow valid http URLs', () => {
expect(sanitizeURL('http://example.com')).toBe('http://example.com');
});
it('should allow valid https URLs', () => {
expect(sanitizeURL('https://example.com')).toBe('https://example.com');
});
it('should allow mailto URLs', () => {
expect(sanitizeURL('mailto:test@example.com')).toBe('mailto:test@example.com');
});
it('should reject javascript URLs', () => {
expect(sanitizeURL('javascript:alert(1)')).toBe('');
});
it('should reject data URLs', () => {
expect(sanitizeURL('data:text/html,<script>alert(1)</script>')).toBe('');
});
});
describe('escapeHTML', () => {
it('should escape HTML special characters', () => {
expect(escapeHTML('<div>')).toBe('&lt;div&gt;');
expect(escapeHTML('&')).toBe('&amp;');
expect(escapeHTML('"')).toBe('&quot;');
expect(escapeHTML("'")).toBe('&#x27;');
});
it('should handle mixed content', () => {
expect(escapeHTML('<script>alert("test")</script>')).toBe('&lt;script&gt;alert(&quot;test&quot;)&lt;&#x2F;script&gt;');
});
it('should handle empty string', () => {
expect(escapeHTML('')).toBe('');
});
});
});