From 59a9dbb695b53e498faeb31734e2189c501b60bf Mon Sep 17 00:00:00 2001 From: Tsuki <976499226@qq.com> Date: Wed, 30 Jul 2025 18:34:47 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Support=20ga4=20provider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/ga4.ts | 165 ++++++++++++++++++++++++++ src/config.ts | 15 +++ src/index.ts | 3 +- src/providers/ga4.ts | 268 +++++++++++++++++++++++++++++++++++++++++++ src/types.ts | 11 +- 5 files changed, 459 insertions(+), 3 deletions(-) create mode 100644 examples/ga4.ts create mode 100644 src/providers/ga4.ts diff --git a/examples/ga4.ts b/examples/ga4.ts new file mode 100644 index 0000000..f1df8b9 --- /dev/null +++ b/examples/ga4.ts @@ -0,0 +1,165 @@ +/** + * Google Analytics 4 Provider Example + * + * This example demonstrates how to use the GA4 provider with Lobe Analytics + */ +import { createAnalytics } from '../src/config'; + +// Create analytics instance with GA4 provider +const analytics = createAnalytics({ + business: 'myapp', + debug: true, + providers: { + ga4: { + enabled: true, + // Replace with your GA4 Measurement ID + gtagConfig: { + // Optional: GA4 configuration options + debug_mode: true, + }, + measurementId: 'G-XXXXXXXXXX', + }, + }, +}); + +// Example usage function +async function exampleUsage() { + try { + // Initialize analytics + await analytics.initialize(); + console.log('GA4 Analytics initialized successfully'); + + // Track a basic event + await analytics.track({ + name: 'button_click', + properties: { + button_name: 'subscribe_now', + section: 'header', + user_type: 'visitor', + }, + }); + + // Track page view + await analytics.trackPageView('/home', { + referrer: document.referrer, + user_agent: navigator.userAgent, + }); + + // Identify a user (after login) + await analytics.identify('user_12345', { + subscription_level: 'premium', + total_purchases: 5, + user_type: 'customer', + }); + + // Track more events after user identification + await analytics.track({ + name: 'purchase', + properties: { + currency: 'USD', + items: [ + { + category: 'subscription', + item_id: 'product_123', + item_name: 'Premium Subscription', + price: 99.99, + quantity: 1, + }, + ], + transaction_id: 'txn_abc123', + value: 99.99, + }, + }); + + // Use predefined events for type safety + await analytics.trackEvent('user_login', { + method: 'google', + user_type: 'returning', + }); + + // Set global context that will be added to all future events + analytics.setGlobalContext({ + app_version: '1.2.3', + experiment_variant: 'control', + }); + + // Reset user identity (on logout) + await analytics.reset(); + + console.log('All GA4 events tracked successfully'); + } catch (error) { + console.error('GA4 Analytics error:', error); + } +} + +// Advanced usage: Direct access to gtag +async function advancedUsage() { + await analytics.initialize(); + + // Get the GA4 provider for direct access + const ga4Provider = analytics.getProvider('ga4'); + + if (ga4Provider) { + // Get native gtag function for advanced GA4 features + const gtag = (ga4Provider as any).getNativeInstance?.(); + + if (gtag) { + // Direct gtag calls (remember to add business context manually) + gtag('event', 'custom_conversion', { + business: 'myapp', + currency: 'USD', + // Remember to add business context for consistency + spm: 'myapp.checkout', + value: 25.99, // Add spm for tracking hierarchy + }); + + // Set up custom audience events + gtag('event', 'add_to_wishlist', { + business: 'myapp', + item_id: 'product_456', + spm: 'myapp.product_page', + }); + + // Configure enhanced measurement + gtag('config', (ga4Provider as any).getMeasurementId?.(), { + enhanced_conversions: true, + user_id: 'user_12345', + }); + } + + // Get current business context + console.log('Current business:', (ga4Provider as any).getCurrentBusiness?.()); + console.log('Measurement ID:', (ga4Provider as any).getMeasurementId?.()); + } +} + +// Error handling example +async function errorHandlingExample() { + try { + await analytics.initialize(); + + // Track event with potential errors + await analytics.track({ + name: 'error_occurred', + properties: { + error_message: 'Invalid email format', + error_type: 'validation_error', + page: window.location.pathname, + }, + }); + } catch (error) { + console.error('Failed to track error event:', error); + } +} + +// Export functions for testing/demo purposes +export { advancedUsage, analytics, errorHandlingExample, exampleUsage }; + +// Run example if this file is executed directly +if (typeof window !== 'undefined') { + // Browser environment - you can call these functions manually + console.log('GA4 Analytics Example loaded. Call exampleUsage() to test.'); + + // Uncomment to run automatically: + // exampleUsage(); +} diff --git a/src/config.ts b/src/config.ts index b5ed765..4357bcf 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,4 +1,5 @@ import { AnalyticsManager } from './manager'; +import { GoogleAnalyticsProvider } from './providers/ga4'; import { PostHogAnalyticsProvider } from './providers/posthog'; import type { AnalyticsConfig } from './types'; @@ -11,6 +12,7 @@ import type { AnalyticsConfig } from './types'; * @example * ```typescript * const analytics = createAnalytics({ + * business: 'myapp', * debug: true, * providers: { * posthog: { @@ -18,6 +20,13 @@ import type { AnalyticsConfig } from './types'; * key: 'phc_your_key', * host: 'https://app.posthog.com', * }, + * ga4: { + * enabled: true, + * measurementId: 'G-XXXXXXXXXX', + * gtagConfig: { + * debug_mode: true, + * }, + * }, * }, * }); * @@ -33,6 +42,12 @@ export function createAnalytics(config: AnalyticsConfig): AnalyticsManager { manager.registerProvider('posthog', provider); } + // Register Google Analytics 4 if enabled + if (config.providers.ga4?.enabled) { + const provider = new GoogleAnalyticsProvider(config.providers.ga4, config.business); + manager.registerProvider('ga4', provider); + } + // Note: posthogNode provider is not available in the client entry point // Use '@lobehub/analytics/server' for server-side analytics if (config.providers.posthogNode?.enabled) { diff --git a/src/index.ts b/src/index.ts index c93c684..ff3c9e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ export { BaseAnalytics } from './base'; export { AnalyticsManager } from './manager'; // Providers +export { GoogleAnalyticsProvider } from './providers/ga4'; export { PostHogAnalyticsProvider } from './providers/posthog'; // Note: PostHogNodeAnalyticsProvider is available in '@lobehub/analytics/server' @@ -32,7 +33,7 @@ export type { AnalyticsConfig, AnalyticsEvent, EventContext, - GoogleProviderAnalyticsConfig, + GoogleAnalyticsProviderConfig, PostHogNodeProviderAnalyticsConfig, PostHogProviderAnalyticsConfig, PredefinedEvents, diff --git a/src/providers/ga4.ts b/src/providers/ga4.ts new file mode 100644 index 0000000..b523030 --- /dev/null +++ b/src/providers/ga4.ts @@ -0,0 +1,268 @@ +/** + * Google Analytics 4 Provider + * Uses gtag.js library for client-side tracking + */ +import { BaseAnalytics } from '@/base'; +import type { AnalyticsEvent, GoogleAnalyticsProviderConfig } from '@/types'; + +/** + * Google Analytics 4 Analytics Provider + * Uses gtag.js for tracking events, page views, and user identification + */ +export class GoogleAnalyticsProvider extends BaseAnalytics { + private readonly config: GoogleAnalyticsProviderConfig; + private initialized = false; + + constructor(config: GoogleAnalyticsProviderConfig, business: string) { + super({ business, debug: config.debug, enabled: config.enabled }); + this.config = config; + } + + getProviderName(): string { + return 'Google Analytics 4'; + } + + async initialize(): Promise { + if (!this.isEnabled() || this.initialized) { + return; + } + + try { + // Ensure gtag function is available + if (typeof window === 'undefined') { + this.logError('GA4 provider requires browser environment'); + return; + } + + // Initialize dataLayer if not exists + (window as any).dataLayer = (window as any).dataLayer || []; + const gtag = + (window as any).gtag || + function () { + (window as any).dataLayer.push(arguments); + }; + (window as any).gtag = gtag; + + // Check if gtag.js is already loaded or if gtag function exists + const gtagExists = typeof (window as any).gtag === 'function'; + + // Only load script if gtag function doesn't exist + if (!gtagExists) { + // Check for existing gtag script + const existingScript = document.querySelector( + `script[src*="gtag/js"], script[id*="ga"], script[id*="gtag"]`, + ); + + if (!existingScript) { + const script = document.createElement('script'); + script.async = true; + script.src = `https://www.googletagmanager.com/gtag/js?id=${this.config.measurementId}`; + document.head.append(script); + } + } + + // Initialize gtag + gtag('js', new Date()); + + // Configure GA4 with user config and our defaults + const configOptions = { + // User's gtag config options + ...this.config.gtagConfig, + // Our internal config (these override user config for consistency) + debug_mode: this.debug || this.config.gtagConfig?.debug_mode, + }; + + gtag('config', this.config.measurementId, configOptions); + + this.initialized = true; + this.log('Google Analytics 4 initialized successfully'); + this.log(`Measurement ID: ${this.config.measurementId}`); + this.log(`Business context will be added to all events: ${this.business}`); + } catch (error) { + this.logError('Failed to initialize Google Analytics 4', error); + throw error; + } + } + + async track(event: AnalyticsEvent): Promise { + if (!this.isEnabled() || !this.initialized || !this.validateEvent(event)) { + return; + } + + try { + const gtag = (window as any).gtag; + if (!gtag) { + this.logError('gtag function not available'); + return; + } + + const enrichedProperties = this.enrichProperties(event.properties); + + // Send event with enriched properties + const eventParams = { + ...enrichedProperties, + // Add user_id if provided in the event + ...(event.userId && { user_id: event.userId }), + }; + + gtag('event', event.name, eventParams); + + this.log(`Tracked event: ${event.name}`, { ...event, properties: enrichedProperties }); + } catch (error) { + this.logError(`Failed to track event: ${event.name}`, error); + } + } + + async identify(userId: string, properties?: Record): Promise { + if (!this.isEnabled() || !this.initialized) { + return; + } + + try { + const gtag = (window as any).gtag; + if (!gtag) { + this.logError('gtag function not available'); + return; + } + + // 1. Set user_id in config (affects all subsequent events) + gtag('config', this.config.measurementId, { + user_id: userId, + }); + + // 2. Set user properties if provided + if (properties && Object.keys(properties).length > 0) { + const enrichedProperties = this.enrichProperties(properties); + gtag('set', { + user_properties: enrichedProperties, + }); + } + + // 3. Track login event (GA4 recommended practice) + gtag('event', 'login', { + user_id: userId, + ...this.enrichProperties(), + }); + + this.log(`Identified user: ${userId}`, properties); + } catch (error) { + this.logError(`Failed to identify user: ${userId}`, error); + } + } + + async trackPageView(page: string, properties?: Record): Promise { + if (!this.isEnabled() || !this.initialized) { + return; + } + + try { + const enrichedProperties = this.enrichProperties(properties); + + // Use the track method to send page_view event + await this.track({ + name: 'page_view', + properties: { + page_location: page, + page_title: page, + ...enrichedProperties, + }, + }); + + this.log(`Tracked page view: ${page}`, enrichedProperties); + } catch (error) { + this.logError(`Failed to track page view: ${page}`, error); + } + } + + async reset(): Promise { + if (!this.isEnabled() || !this.initialized) { + return; + } + + try { + const gtag = (window as any).gtag; + if (!gtag) { + this.logError('gtag function not available'); + return; + } + + // 1. Clear user_id (set to null, not string "null") + gtag('config', this.config.measurementId, { + user_id: null, + }); + + // 2. Clear user properties + gtag('set', { + user_properties: {}, + }); + + // 3. Track logout event + gtag('event', 'logout', this.enrichProperties()); + + this.log('Reset user identity and tracked logout'); + } catch (error) { + this.logError('Failed to reset user identity', error); + } + } + + /** + * Check if feature flag is enabled + * Note: GA4 doesn't have built-in feature flags like PostHog, + * so this always returns false + */ + isFeatureEnabled(flag: string): boolean { + this.log(`Feature flags not supported in GA4. Flag "${flag}" returns false`); + return false; + } + + /** + * Get the native gtag function for direct access to GA4 APIs + * + * Note: When using the native gtag function directly, events will NOT automatically + * include the business and spm properties. You need to add them manually if desired. + * + * @returns gtag function or null if not initialized + * + * @example + * ```typescript + * const analytics = createAnalytics({ business: 'myapp', ... }); + * const ga4Provider = analytics.getProvider('ga4'); + * const gtag = ga4Provider.getNativeInstance(); + * + * // Manual business context addition required for direct calls + * gtag?.('event', 'custom_event', { + * custom: 'data', + * business: 'myapp', + * spm: 'myapp.custom_section' + * }); + * ``` + */ + getNativeInstance(): ((...args: any[]) => void) | null { + if (!this.isEnabled() || !this.initialized) { + this.log('Cannot get native instance: provider not enabled or not initialized'); + return null; + } + + const gtag = (window as any).gtag; + if (!gtag) { + this.log('gtag function not available'); + return null; + } + + return gtag; + } + + /** + * Get current measurement ID + */ + getMeasurementId(): string { + return this.config.measurementId; + } + + /** + * Get current business context + */ + getCurrentBusiness(): string { + return this.business; + } +} diff --git a/src/types.ts b/src/types.ts index 49240d6..65fd329 100644 --- a/src/types.ts +++ b/src/types.ts @@ -81,7 +81,13 @@ export interface UmamiProviderAnalyticsConfig extends ProviderConfig { websiteId: string; } -export interface GoogleProviderAnalyticsConfig extends ProviderConfig { +export interface GoogleAnalyticsProviderConfig extends ProviderConfig { + // GA4 gtag config options - integrates with official gtag types when available + gtagConfig?: { + // Allow additional gtag config options + [key: string]: any; + debug_mode?: boolean; + }; measurementId: string; } @@ -90,7 +96,7 @@ export interface AnalyticsConfig { business: string; debug?: boolean; providers: { - ga?: GoogleProviderAnalyticsConfig; + ga4?: GoogleAnalyticsProviderConfig; posthog?: PostHogProviderAnalyticsConfig; posthogNode?: PostHogNodeProviderAnalyticsConfig; umami?: UmamiProviderAnalyticsConfig; @@ -100,6 +106,7 @@ export interface AnalyticsConfig { // Provider type mapping for type-safe provider access export interface ProviderTypeMap { + ga4: import('./providers/ga4').GoogleAnalyticsProvider; posthog: import('./providers/posthog').PostHogAnalyticsProvider; posthogNode: import('./providers/posthog-node').PostHogNodeAnalyticsProvider; // Add more providers as they are implemented