311 lines
7.7 KiB
TypeScript
311 lines
7.7 KiB
TypeScript
/**
|
|
* Analytics utilities and tracking setup
|
|
* Supports Google Analytics, Plausible, and custom tracking
|
|
*/
|
|
|
|
// Types for analytics events
|
|
export interface AnalyticsEvent {
|
|
action: string
|
|
category: string
|
|
label?: string
|
|
value?: number
|
|
}
|
|
|
|
export interface PageViewEvent {
|
|
page_title: string
|
|
page_location: string
|
|
page_path: string
|
|
}
|
|
|
|
export interface CustomEvent {
|
|
event_name: string
|
|
[key: string]: any
|
|
}
|
|
|
|
// Analytics providers configuration
|
|
interface AnalyticsConfig {
|
|
googleAnalytics?: {
|
|
measurementId: string
|
|
enabled: boolean
|
|
}
|
|
plausible?: {
|
|
domain: string
|
|
enabled: boolean
|
|
}
|
|
customTracking?: {
|
|
enabled: boolean
|
|
endpoint?: string
|
|
}
|
|
debug?: boolean
|
|
}
|
|
|
|
class Analytics {
|
|
private config: AnalyticsConfig
|
|
private isInitialized = false
|
|
|
|
constructor(config: AnalyticsConfig) {
|
|
this.config = config
|
|
}
|
|
|
|
// Initialize analytics
|
|
init() {
|
|
if (this.isInitialized || typeof window === 'undefined') return
|
|
|
|
// Initialize Google Analytics
|
|
if (this.config.googleAnalytics?.enabled && this.config.googleAnalytics.measurementId) {
|
|
this.initGoogleAnalytics(this.config.googleAnalytics.measurementId)
|
|
}
|
|
|
|
// Initialize Plausible
|
|
if (this.config.plausible?.enabled && this.config.plausible.domain) {
|
|
this.initPlausible(this.config.plausible.domain)
|
|
}
|
|
|
|
this.isInitialized = true
|
|
|
|
if (this.config.debug) {
|
|
console.log('Analytics initialized with config:', this.config)
|
|
}
|
|
}
|
|
|
|
// Initialize Google Analytics
|
|
private initGoogleAnalytics(measurementId: string) {
|
|
// Load gtag script
|
|
const script1 = document.createElement('script')
|
|
script1.async = true
|
|
script1.src = `https://www.googletagmanager.com/gtag/js?id=${measurementId}`
|
|
document.head.appendChild(script1)
|
|
|
|
// Initialize gtag
|
|
const script2 = document.createElement('script')
|
|
script2.innerHTML = `
|
|
window.dataLayer = window.dataLayer || [];
|
|
function gtag(){dataLayer.push(arguments);}
|
|
gtag('js', new Date());
|
|
gtag('config', '${measurementId}', {
|
|
page_title: document.title,
|
|
page_location: window.location.href,
|
|
});
|
|
`
|
|
document.head.appendChild(script2)
|
|
|
|
// Make gtag available globally
|
|
;(window as any).gtag =
|
|
(window as any).gtag ||
|
|
function () {
|
|
;(window as any).dataLayer = (window as any).dataLayer || []
|
|
;(window as any).dataLayer.push(arguments)
|
|
}
|
|
}
|
|
|
|
// Initialize Plausible
|
|
private initPlausible(domain: string) {
|
|
const script = document.createElement('script')
|
|
script.defer = true
|
|
script.setAttribute('data-domain', domain)
|
|
script.src = 'https://plausible.io/js/script.js'
|
|
document.head.appendChild(script)
|
|
}
|
|
|
|
// Track page view
|
|
pageView(event: Partial<PageViewEvent> = {}) {
|
|
if (typeof window === 'undefined') return
|
|
|
|
const pageViewData = {
|
|
page_title: document.title,
|
|
page_location: window.location.href,
|
|
page_path: window.location.pathname,
|
|
...event,
|
|
}
|
|
|
|
// Google Analytics
|
|
if (this.config.googleAnalytics?.enabled && (window as any).gtag) {
|
|
;(window as any).gtag('config', this.config.googleAnalytics.measurementId, {
|
|
page_title: pageViewData.page_title,
|
|
page_location: pageViewData.page_location,
|
|
})
|
|
}
|
|
|
|
// Plausible (automatically tracks page views)
|
|
|
|
// Custom tracking
|
|
if (this.config.customTracking?.enabled) {
|
|
this.customTrack('page_view', pageViewData)
|
|
}
|
|
|
|
if (this.config.debug) {
|
|
console.log('Page view tracked:', pageViewData)
|
|
}
|
|
}
|
|
|
|
// Track custom event
|
|
track(eventName: string, properties: Record<string, any> = {}) {
|
|
if (typeof window === 'undefined') return
|
|
|
|
// Google Analytics
|
|
if (this.config.googleAnalytics?.enabled && (window as any).gtag) {
|
|
;(window as any).gtag('event', eventName, properties)
|
|
}
|
|
|
|
// Plausible
|
|
if (this.config.plausible?.enabled && (window as any).plausible) {
|
|
;(window as any).plausible(eventName, { props: properties })
|
|
}
|
|
|
|
// Custom tracking
|
|
if (this.config.customTracking?.enabled) {
|
|
this.customTrack(eventName, properties)
|
|
}
|
|
|
|
if (this.config.debug) {
|
|
console.log('Event tracked:', eventName, properties)
|
|
}
|
|
}
|
|
|
|
// Track user signup
|
|
trackSignup(method: string = 'email') {
|
|
this.track('sign_up', { method })
|
|
}
|
|
|
|
// Track user login
|
|
trackLogin(method: string = 'email') {
|
|
this.track('login', { method })
|
|
}
|
|
|
|
// Track user logout
|
|
trackLogout() {
|
|
this.track('logout')
|
|
}
|
|
|
|
// Track form submission
|
|
trackFormSubmit(formName: string, success: boolean = true) {
|
|
this.track('form_submit', {
|
|
form_name: formName,
|
|
success,
|
|
})
|
|
}
|
|
|
|
// Track button click
|
|
trackButtonClick(buttonName: string, location?: string) {
|
|
this.track('button_click', {
|
|
button_name: buttonName,
|
|
location,
|
|
})
|
|
}
|
|
|
|
// Track search
|
|
trackSearch(searchTerm: string, resultCount?: number) {
|
|
this.track('search', {
|
|
search_term: searchTerm,
|
|
result_count: resultCount,
|
|
})
|
|
}
|
|
|
|
// Track file download
|
|
trackDownload(fileName: string, fileType?: string) {
|
|
this.track('file_download', {
|
|
file_name: fileName,
|
|
file_type: fileType,
|
|
})
|
|
}
|
|
|
|
// Custom tracking implementation
|
|
private async customTrack(eventName: string, properties: Record<string, any>) {
|
|
if (!this.config.customTracking?.endpoint) return
|
|
|
|
try {
|
|
await fetch(this.config.customTracking.endpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
event: eventName,
|
|
properties: {
|
|
...properties,
|
|
timestamp: new Date().toISOString(),
|
|
user_agent: navigator.userAgent,
|
|
url: window.location.href,
|
|
},
|
|
}),
|
|
})
|
|
} catch (error) {
|
|
if (this.config.debug) {
|
|
console.error('Custom tracking failed:', error)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Identify user
|
|
identify(userId: string, traits: Record<string, any> = {}) {
|
|
// Google Analytics
|
|
if (this.config.googleAnalytics?.enabled && (window as any).gtag) {
|
|
;(window as any).gtag('config', this.config.googleAnalytics.measurementId, {
|
|
user_id: userId,
|
|
custom_map: traits,
|
|
})
|
|
}
|
|
|
|
// Custom tracking
|
|
if (this.config.customTracking?.enabled) {
|
|
this.customTrack('identify', { user_id: userId, traits })
|
|
}
|
|
|
|
if (this.config.debug) {
|
|
console.log('User identified:', userId, traits)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create analytics instance
|
|
const analyticsConfig: AnalyticsConfig = {
|
|
googleAnalytics: {
|
|
measurementId: process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || '',
|
|
enabled: !!(process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID && process.env.NODE_ENV === 'production'),
|
|
},
|
|
plausible: {
|
|
domain: process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN || '',
|
|
enabled: !!(process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN && process.env.NODE_ENV === 'production'),
|
|
},
|
|
customTracking: {
|
|
enabled: false,
|
|
endpoint: process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT,
|
|
},
|
|
debug: process.env.NODE_ENV === 'development',
|
|
}
|
|
|
|
export const analytics = new Analytics(analyticsConfig)
|
|
|
|
// React hook for analytics
|
|
import { useEffect } from 'react'
|
|
import { usePathname } from 'next/navigation'
|
|
|
|
export function useAnalytics() {
|
|
const pathname = usePathname()
|
|
|
|
useEffect(() => {
|
|
analytics.init()
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
analytics.pageView({
|
|
page_path: pathname,
|
|
})
|
|
}, [pathname])
|
|
|
|
return analytics
|
|
}
|
|
|
|
// Higher-order component for analytics
|
|
import React from 'react'
|
|
|
|
export function withAnalytics<P extends object>(Component: React.ComponentType<P>) {
|
|
return function AnalyticsWrapper(props: P) {
|
|
useAnalytics()
|
|
return <Component {...props} />
|
|
}
|
|
}
|
|
|
|
// Export analytics instance
|
|
export default analytics
|