// services/analytics.service.js const { BetaAnalyticsDataClient } = require('@google-analytics/data'); const Analytics = require('../models/analytics.model'); const { createClient } = require('redis'); const logger = require('../utils/logger'); class AnalyticsService { constructor() { // Initialize Redis client this.redis = createClient({ url: process.env.REDIS_URL }); this.redis.on('error', err => logger.error('Redis Client Error:', err)); this.redis.connect().catch(err => logger.error('Redis connection error:', err)); // Initialize GA4 client this.analyticsClient = new BetaAnalyticsDataClient({ credentials: JSON.parse(process.env.GOOGLE_APPLICATION_CREDENTIALS_JSON) }); this.propertyId = process.env.GA_PROPERTY_ID; } async getBasicMetrics(params = {}) { const cacheKey = `analytics:basic_metrics:${JSON.stringify(params)}`; logger.info(`Fetching basic metrics with params:`, params); try { // Try Redis first const cachedData = await this.redis.get(cacheKey); if (cachedData) { logger.info('Analytics metrics found in Redis cache'); return JSON.parse(cachedData); } // Check MongoDB using new findValidCache method const mongoData = await Analytics.findValidCache('basic_metrics', params); if (mongoData) { logger.info('Analytics metrics found in MongoDB'); const formattedData = mongoData.formatResponse(); await this.redis.set(cacheKey, JSON.stringify(formattedData), { EX: Analytics.getCacheDuration('basic_metrics') }); return formattedData; } // Fetch fresh data from GA4 logger.info('Fetching fresh metrics data from GA4'); const [response] = await this.analyticsClient.runReport({ property: `properties/${this.propertyId}`, dateRanges: [{ startDate: params.startDate || '7daysAgo', endDate: 'today' }], dimensions: [{ name: 'date' }], metrics: [ { name: 'activeUsers' }, { name: 'newUsers' }, { name: 'averageSessionDuration' }, { name: 'screenPageViews' }, { name: 'bounceRate' }, { name: 'conversions' } ], returnPropertyQuota: true }); // Create new Analytics document with fresh data const analyticsDoc = await Analytics.create({ type: 'basic_metrics', params, data: response, quotaInfo: response.propertyQuota }); const formattedData = analyticsDoc.formatResponse(); // Save to Redis await this.redis.set(cacheKey, JSON.stringify(formattedData), { EX: Analytics.getCacheDuration('basic_metrics') }); return formattedData; } catch (error) { logger.error('Error fetching analytics metrics:', { error: error.message, stack: error.stack }); throw error; } } async getRealTimeBasicData() { const cacheKey = 'analytics:realtime:basic'; logger.info('Fetching realtime basic data'); try { // Try Redis first const [cachedData, lastUpdated] = await Promise.all([ this.redis.get(cacheKey), this.redis.get(`${cacheKey}:lastUpdated`) ]); if (cachedData) { logger.info('Realtime basic data found in Redis cache:', cachedData); return { ...JSON.parse(cachedData), lastUpdated: lastUpdated ? new Date(lastUpdated).toISOString() : new Date().toISOString() }; } // Fetch fresh data logger.info(`Fetching fresh realtime data from GA4 server`); const [userResponse] = await this.analyticsClient.runRealtimeReport({ property: `properties/${this.propertyId}`, metrics: [{ name: 'activeUsers' }], returnPropertyQuota: true }); logger.info('GA4 user response:', userResponse); const [fiveMinResponse] = await this.analyticsClient.runRealtimeReport({ property: `properties/${this.propertyId}`, metrics: [{ name: 'activeUsers' }], minuteRanges: [{ startMinutesAgo: 5, endMinutesAgo: 0 }] }); const [timeSeriesResponse] = await this.analyticsClient.runRealtimeReport({ property: `properties/${this.propertyId}`, dimensions: [{ name: 'minutesAgo' }], metrics: [{ name: 'activeUsers' }] }); // Create new Analytics document const analyticsDoc = await Analytics.create({ type: 'realtime_basic', data: { userResponse, fiveMinResponse, timeSeriesResponse, quotaInfo: { projectHourly: userResponse.propertyQuota.tokensPerProjectPerHour, daily: userResponse.propertyQuota.tokensPerDay, serverErrors: userResponse.propertyQuota.serverErrorsPerProjectPerHour, thresholdedRequests: userResponse.propertyQuota.potentiallyThresholdedRequestsPerHour } }, quotaInfo: userResponse.propertyQuota }); const formattedData = analyticsDoc.formatResponse(); // Save to Redis await this.redis.set(cacheKey, JSON.stringify(formattedData), { EX: Analytics.getCacheDuration('realtime_basic') }); return formattedData; } catch (error) { logger.error('Detailed error in getRealTimeBasicData:', { message: error.message, stack: error.stack, code: error.code, response: error.response?.data }); throw error; } } async getRealTimeDetailedData() { const cacheKey = 'analytics:realtime:detailed'; logger.info('Fetching realtime detailed data'); try { // Check Redis first const cachedData = await this.redis.get(cacheKey); if (cachedData) { logger.info('Realtime detailed data found in Redis cache'); return JSON.parse(cachedData); } // Fetch fresh data from GA4 const [pageResponse] = await this.analyticsClient.runRealtimeReport({ property: `properties/${this.propertyId}`, dimensions: [{ name: 'unifiedScreenName' }], metrics: [{ name: 'screenPageViews' }], orderBy: [{ metric: { metricName: 'screenPageViews' }, desc: true }], limit: 25 }); const [eventResponse] = await this.analyticsClient.runRealtimeReport({ property: `properties/${this.propertyId}`, dimensions: [{ name: 'eventName' }], metrics: [{ name: 'eventCount' }], orderBy: [{ metric: { metricName: 'eventCount' }, desc: true }], limit: 25 }); 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 }); // Create new Analytics document const analyticsDoc = await Analytics.create({ type: 'realtime_detailed', data: { pageResponse, eventResponse, sourceResponse: deviceResponse }, quotaInfo: deviceResponse.propertyQuota }); const formattedData = analyticsDoc.formatResponse(); // Save to Redis await this.redis.set(cacheKey, JSON.stringify(formattedData), { EX: Analytics.getCacheDuration('realtime_detailed') }); return formattedData; } catch (error) { logger.error('Error fetching realtime detailed data:', { error: error.message, stack: error.stack }); throw error; } } async getUserBehavior(params = {}) { const cacheKey = `analytics:user_behavior:${JSON.stringify(params)}`; const timeRange = params.timeRange || '7'; logger.info('Fetching user behavior data', { params }); try { // Try Redis first const cachedData = await this.redis.get(cacheKey); if (cachedData) { logger.info('User behavior data found in Redis cache'); return JSON.parse(cachedData); } // Check MongoDB using new findValidCache method const mongoData = await Analytics.findValidCache('user_behavior', params); if (mongoData) { logger.info('User behavior data found in MongoDB'); const formattedData = mongoData.formatResponse(); await this.redis.set(cacheKey, JSON.stringify(formattedData), { EX: Analytics.getCacheDuration('user_behavior') }); return formattedData; } // Fetch fresh data from GA4 logger.info('Fetching fresh user behavior data from GA4'); 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 }); const [deviceResponse] = await this.analyticsClient.runReport({ property: `properties/${this.propertyId}`, dateRanges: [{ startDate: `${timeRange}daysAgo`, endDate: 'today' }], dimensions: [{ name: 'deviceCategory' }], metrics: [ { name: 'screenPageViews' }, { name: 'sessions' } ] }); 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 }); // Create new Analytics document const analyticsDoc = await Analytics.create({ type: 'user_behavior', params, data: { pageResponse, deviceResponse, sourceResponse }, quotaInfo: sourceResponse.propertyQuota }); const formattedData = analyticsDoc.formatResponse(); // Save to Redis await this.redis.set(cacheKey, JSON.stringify(formattedData), { EX: Analytics.getCacheDuration('user_behavior') }); return formattedData; } catch (error) { logger.error('Error fetching user behavior data:', { error: error.message, stack: error.stack }); throw error; } } } module.exports = new AnalyticsService();