Phase 4 + 6
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user