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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user