// Google Analytics (GA4) service — ESM conversion of google-server/services/analytics.service.js. // Phase 4: accepts injected ioredis client (was self-constructing node-redis v4 before). // node-redis v4 set syntax `{ EX: 300 }` is translated to ioredis `setex(key, 300, val)`. import { BetaAnalyticsDataClient } from '@google-analytics/data'; const CACHE_DURATIONS = { REALTIME_BASIC: 60, REALTIME_DETAILED: 300, BASIC_METRICS: 3600, USER_BEHAVIOR: 3600, }; export class AnalyticsService { constructor(redis) { if (!redis) { throw new Error('AnalyticsService requires an ioredis client (Phase 4: injected)'); } this.redis = redis; 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; } get _redisReady() { return this.redis.status === 'ready' || this.redis.status === 'connect'; } async _cacheGet(key) { if (!this._redisReady) return null; try { const raw = await this.redis.get(key); return raw ? JSON.parse(raw) : null; } catch (err) { console.warn('[AnalyticsService] cache get failed:', err.message); return null; } } async _cacheSet(key, value, ttlSec) { if (!this._redisReady) return; try { await this.redis.setex(key, ttlSec, JSON.stringify(value)); } catch (err) { console.warn('[AnalyticsService] cache set failed:', err.message); } } async getBasicMetrics(startDate = '7daysAgo') { const cacheKey = `analytics:basic_metrics:${startDate}`; const cached = await this._cacheGet(cacheKey); if (cached) return cached; 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, }); await this._cacheSet(cacheKey, response, CACHE_DURATIONS.BASIC_METRICS); return response; } async getRealTimeBasicData() { const cacheKey = 'analytics:realtime:basic'; const cached = await this._cacheGet(cacheKey); if (cached) return cached; const [userResponse] = await this.analyticsClient.runRealtimeReport({ property: `properties/${this.propertyId}`, metrics: [{ name: 'activeUsers' }], returnPropertyQuota: true, }); 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' }], }); const response = { userResponse, fiveMinResponse, timeSeriesResponse, quotaInfo: { projectHourly: userResponse.propertyQuota.tokensPerProjectPerHour, daily: userResponse.propertyQuota.tokensPerDay, serverErrors: userResponse.propertyQuota.serverErrorsPerProjectPerHour, thresholdedRequests: userResponse.propertyQuota.potentiallyThresholdedRequestsPerHour, }, }; await this._cacheSet(cacheKey, response, CACHE_DURATIONS.REALTIME_BASIC); return response; } async getRealTimeDetailedData() { const cacheKey = 'analytics:realtime:detailed'; const cached = await this._cacheGet(cacheKey); if (cached) return cached; 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, }); const response = { pageResponse, eventResponse, sourceResponse: deviceResponse, }; await this._cacheSet(cacheKey, response, CACHE_DURATIONS.REALTIME_DETAILED); return response; } async getUserBehavior(timeRange = '30') { const cacheKey = `analytics:user_behavior:${timeRange}`; const cached = await this._cacheGet(cacheKey); if (cached) return cached; 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, }); const response = { pageResponse, deviceResponse, sourceResponse }; await this._cacheSet(cacheKey, response, CACHE_DURATIONS.USER_BEHAVIOR); return response; } }