Implement GA4 service with Measurement Protocol calls FRE-5280

- Real GA4 Measurement Protocol implementation (page_view, purchase,
  waitlist_signup, conversion tracking)
- Setup script with manual and automated (GCP Admin API) paths
- GA4 env vars documented in .env.example

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-05-14 09:22:36 -04:00
parent 74949d9bcc
commit 9858834a67
3 changed files with 268 additions and 58 deletions

View File

@@ -1,66 +1,71 @@
import { google } from 'googleapis';
import { analyticsEnv, EventType } from '../config/analytics.config';
import { analyticsEnv } from '../config/analytics.config';
const GA4_ENDPOINT = 'https://www.google-analytics.com/mp/collect';
// GA4 service
export class GA4Service {
private auth: any;
private measurementId: string;
private apiSecret: string;
private initialized = false;
constructor() {
this.auth = google.auth.fromAPIKey(analyticsEnv.GA4_API_SECRET || 'placeholder');
this.measurementId = analyticsEnv.GA4_MEASUREMENT_ID;
this.apiSecret = analyticsEnv.GA4_API_SECRET || '';
}
/**
* Initialize GA4 client
*/
async initialize(): Promise<void> {
// TODO: Initialize GA4 client with measurement ID
console.log('GA4 client initialized');
}
/**
* Send event to GA4
*/
async sendEvent(
eventName: string,
params: {
client_id: string;
[key: string]: any;
if (!this.measurementId || this.measurementId === 'placeholder') {
console.warn('GA4: no measurement ID configured — events will be dropped');
return;
}
if (!this.apiSecret) {
console.warn('GA4: no API secret configured — events will be dropped');
return;
}
this.initialized = true;
}
private async post(eventName: string, params: Record<string, unknown>): Promise<void> {
if (!this.initialized) {
console.log('GA4 (dry):', eventName, params);
return;
}
try {
const url = `${GA4_ENDPOINT}?measurement_id=${this.measurementId}&api_secret=${this.apiSecret}`;
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: params.client_id || 'system',
events: [{ name: eventName, params }],
}),
});
if (!res.ok && res.status !== 204) {
console.error(`GA4 error: ${res.status} for event ${eventName}`);
}
} catch (err) {
console.error('GA4 send failed:', err);
}
): Promise<void> {
// TODO: Implement GA4 event tracking
// const measurementId = analyticsEnv.GA4_MEASUREMENT_ID;
// await fetch(`https://www.google-analytics.com/mp/collect?measurement_id=${measurementId}&api_secret=${analyticsEnv.GA4_API_SECRET}`, {
// method: 'POST',
// body: JSON.stringify({
// events: [{ name: eventName, params }],
// }),
// });
console.log('GA4 event:', eventName, params);
}
/**
* Track page view
*/
async trackPageView(clientId: string, path: string, title?: string): Promise<void> {
await this.sendEvent('page_view', {
await this.post('page_view', {
client_id: clientId,
page_path: path,
page_title: title,
page_title: title || undefined,
engagement_time_msec: 1,
});
}
/**
* Track e-commerce purchase
*/
async trackPurchase(
clientId: string,
transactionId: string,
value: number,
currency: string,
items: Array<{ name: string; price: number; quantity: number }>
items: Array<{ item_id: string; item_name: string; price: number; quantity: number }>,
): Promise<void> {
await this.sendEvent('purchase', {
await this.post('purchase', {
client_id: clientId,
transaction_id: transactionId,
value,
@@ -69,36 +74,61 @@ export class GA4Service {
});
}
/**
* Track conversion
*/
async trackWaitlistSignup(clientId: string, email?: string): Promise<void> {
await this.post('waitlist_signup', {
client_id: clientId,
email_hash: email ? await this.sha256(email) : undefined,
});
}
async trackConversion(
clientId: string,
conversionName: string,
metadata?: Record<string, any>
metadata?: Record<string, unknown>,
): Promise<void> {
await this.sendEvent('conversion', {
await this.post(conversionName, {
client_id: clientId,
conversion_name: conversionName,
...metadata,
});
}
/**
* Get analytics data (for dashboards)
*/
async getMetrics(
dateRange: { startDate: string; endDate: string },
metrics: string[],
dimensions?: string[]
): Promise<any> {
// TODO: Implement GA4 Analytics Data API
return {
rows: [],
totals: [],
};
dimensions?: string[],
): Promise<{ rows: unknown[]; totals: unknown[] }> {
if (!this.measurementId || this.measurementId === 'placeholder') {
return { rows: [], totals: [] };
}
const DataApi = await import('@google-analytics/data').catch(() => null);
if (!DataApi) {
console.warn('GA4: @google-analytics/data not installed — cannot query metrics');
return { rows: [], totals: [] };
}
try {
const client = new DataApi.BetaAnalyticsDataClient();
const [response] = await client.runReport({
property: `properties/${this.measurementId.replace('G-', '')}`,
dateRanges: [dateRange],
metrics: metrics.map(m => ({ name: m })),
dimensions: (dimensions || []).map(d => ({ name: d })),
});
return {
rows: response.rows || [],
totals: response.totals || [],
};
} catch (err) {
console.error('GA4 query failed:', err);
return { rows: [], totals: [] };
}
}
private async sha256(str: string): Promise<string> {
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str.toLowerCase().trim()));
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
}
}
// Export instance
export const ga4Service = new GA4Service();