196 lines
6.6 KiB
JavaScript
196 lines
6.6 KiB
JavaScript
// 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;
|
|
}
|
|
}
|