const { BetaAnalyticsDataClient } = require('@google-analytics/data'); const { createClient } = require('redis'); class AnalyticsService { constructor() { // Initialize Redis client this.redis = createClient({ url: process.env.REDIS_URL || 'redis://localhost:6379' }); this.redis.on('error', err => console.error('Redis Client Error:', err)); this.redis.connect().catch(err => console.error('Redis connection error:', err)); try { // Initialize GA4 client const credentials = process.env.GOOGLE_APPLICATION_CREDENTIALS_JSON; this.analyticsClient = new BetaAnalyticsDataClient({ credentials: typeof credentials === 'string' ? JSON.parse(credentials) : credentials }); this.propertyId = process.env.GA_PROPERTY_ID; } catch (error) { console.error('Failed to initialize GA4 client:', error); throw error; } } // Cache durations CACHE_DURATIONS = { REALTIME_BASIC: 60, // 1 minute REALTIME_DETAILED: 300, // 5 minutes BASIC_METRICS: 3600, // 1 hour USER_BEHAVIOR: 3600 // 1 hour }; async getBasicMetrics(startDate = '7daysAgo') { const cacheKey = `analytics:basic_metrics:${startDate}`; try { // Try Redis first const cachedData = await this.redis.get(cacheKey); if (cachedData) { console.log('Analytics metrics found in Redis cache'); return JSON.parse(cachedData); } // Fetch from GA4 console.log('Fetching fresh metrics data from GA4'); const [response] = await this.analyticsClient.runReport({ property: `properties/${this.propertyId}`, dateRanges: [{ startDate, endDate: 'today' }], dimensions: [{ name: 'date' }], metrics: [ { name: 'activeUsers' }, { name: 'newUsers' }, { name: 'averageSessionDuration' }, { name: 'screenPageViews' }, { name: 'bounceRate' }, { name: 'conversions' } ], returnPropertyQuota: true }); // Cache the response await this.redis.set(cacheKey, JSON.stringify(response), { EX: this.CACHE_DURATIONS.BASIC_METRICS }); return response; } catch (error) { console.error('Error fetching analytics metrics:', { error: error.message, stack: error.stack }); throw error; } } async getRealTimeBasicData() { const cacheKey = 'analytics:realtime:basic'; try { // Try Redis first const cachedData = await this.redis.get(cacheKey); if (cachedData) { console.log('Realtime basic data found in Redis cache'); return JSON.parse(cachedData); } console.log('Fetching fresh realtime data from GA4'); // Fetch active users const [userResponse] = await this.analyticsClient.runRealtimeReport({ property: `properties/${this.propertyId}`, metrics: [{ name: 'activeUsers' }], returnPropertyQuota: true }); // Fetch last 5 minutes const [fiveMinResponse] = await this.analyticsClient.runRealtimeReport({ property: `properties/${this.propertyId}`, metrics: [{ name: 'activeUsers' }], minuteRanges: [{ startMinutesAgo: 5, endMinutesAgo: 0 }] }); // Fetch time series data const [timeSeriesResponse] = await this.analyticsClient.runRealtimeReport({ property: `properties/${this.propertyId}`, dimensions: [{ name: 'minutesAgo' }], metrics: [{ name: 'activeUsers' }] }); const response = { userResponse, fiveMinResponse, timeSeriesResponse, quotaInfo: { projectHourly: userResponse.propertyQuota.tokensPerProjectPerHour, daily: userResponse.propertyQuota.tokensPerDay, serverErrors: userResponse.propertyQuota.serverErrorsPerProjectPerHour, thresholdedRequests: userResponse.propertyQuota.potentiallyThresholdedRequestsPerHour } }; // Cache the response await this.redis.set(cacheKey, JSON.stringify(response), { EX: this.CACHE_DURATIONS.REALTIME_BASIC }); return response; } catch (error) { console.error('Error fetching realtime basic data:', { error: error.message, stack: error.stack }); throw error; } } async getRealTimeDetailedData() { const cacheKey = 'analytics:realtime:detailed'; try { // Try Redis first const cachedData = await this.redis.get(cacheKey); if (cachedData) { console.log('Realtime detailed data found in Redis cache'); return JSON.parse(cachedData); } console.log('Fetching fresh realtime detailed data from GA4'); // Fetch current pages const [pageResponse] = await this.analyticsClient.runRealtimeReport({ property: `properties/${this.propertyId}`, dimensions: [{ name: 'unifiedScreenName' }], metrics: [{ name: 'screenPageViews' }], orderBy: [{ metric: { metricName: 'screenPageViews' }, desc: true }], limit: 25 }); // Fetch events const [eventResponse] = await this.analyticsClient.runRealtimeReport({ property: `properties/${this.propertyId}`, dimensions: [{ name: 'eventName' }], metrics: [{ name: 'eventCount' }], orderBy: [{ metric: { metricName: 'eventCount' }, desc: true }], limit: 25 }); // Fetch device categories const [deviceResponse] = await this.analyticsClient.runRealtimeReport({ property: `properties/${this.propertyId}`, dimensions: [{ name: 'deviceCategory' }], metrics: [{ name: 'activeUsers' }], orderBy: [{ metric: { metricName: 'activeUsers' }, desc: true }], limit: 10, returnPropertyQuota: true }); const response = { pageResponse, eventResponse, sourceResponse: deviceResponse }; // Cache the response await this.redis.set(cacheKey, JSON.stringify(response), { EX: this.CACHE_DURATIONS.REALTIME_DETAILED }); return response; } catch (error) { console.error('Error fetching realtime detailed data:', { error: error.message, stack: error.stack }); throw error; } } async getUserBehavior(timeRange = '30') { const cacheKey = `analytics:user_behavior:${timeRange}`; try { // Try Redis first const cachedData = await this.redis.get(cacheKey); if (cachedData) { console.log('User behavior data found in Redis cache'); return JSON.parse(cachedData); } console.log('Fetching fresh user behavior data from GA4'); // Fetch page data const [pageResponse] = await this.analyticsClient.runReport({ property: `properties/${this.propertyId}`, dateRanges: [{ startDate: `${timeRange}daysAgo`, endDate: 'today' }], dimensions: [{ name: 'pagePath' }], metrics: [ { name: 'screenPageViews' }, { name: 'averageSessionDuration' }, { name: 'bounceRate' }, { name: 'sessions' } ], orderBy: [{ metric: { metricName: 'screenPageViews' }, desc: true }], limit: 25 }); // Fetch device data const [deviceResponse] = await this.analyticsClient.runReport({ property: `properties/${this.propertyId}`, dateRanges: [{ startDate: `${timeRange}daysAgo`, endDate: 'today' }], dimensions: [{ name: 'deviceCategory' }], metrics: [ { name: 'screenPageViews' }, { name: 'sessions' } ] }); // Fetch source data const [sourceResponse] = await this.analyticsClient.runReport({ property: `properties/${this.propertyId}`, dateRanges: [{ startDate: `${timeRange}daysAgo`, endDate: 'today' }], dimensions: [{ name: 'sessionSource' }], metrics: [ { name: 'sessions' }, { name: 'conversions' } ], orderBy: [{ metric: { metricName: 'sessions' }, desc: true }], limit: 25, returnPropertyQuota: true }); const response = { pageResponse, deviceResponse, sourceResponse }; // Cache the response await this.redis.set(cacheKey, JSON.stringify(response), { EX: this.CACHE_DURATIONS.USER_BEHAVIOR }); return response; } catch (error) { console.error('Error fetching user behavior data:', { error: error.message, stack: error.stack }); throw error; } } } module.exports = new AnalyticsService();