diff --git a/.env.example b/.env.example index 6d3b680..244443c 100644 --- a/.env.example +++ b/.env.example @@ -23,3 +23,7 @@ SENTRY_DSN="" SENTRY_ENVIRONMENT="development" SENTRY_RELEASE="0.1.0" SENTRY_TRACES_SAMPLE_RATE="0.1" + +# Google Analytics 4 +GA4_MEASUREMENT_ID="" +GA4_API_SECRET="" diff --git a/packages/shared-analytics/src/services/ga4.service.ts b/packages/shared-analytics/src/services/ga4.service.ts index f8ef9e4..4e25217 100644 --- a/packages/shared-analytics/src/services/ga4.service.ts +++ b/packages/shared-analytics/src/services/ga4.service.ts @@ -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 { - // 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): Promise { + 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 { - // 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 { - 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 { - 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 { + await this.post('waitlist_signup', { + client_id: clientId, + email_hash: email ? await this.sha256(email) : undefined, + }); + } + async trackConversion( clientId: string, conversionName: string, - metadata?: Record + metadata?: Record, ): Promise { - 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 { - // 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 { + 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(); diff --git a/scripts/setup-ga4.sh b/scripts/setup-ga4.sh new file mode 100755 index 0000000..2c8dfce --- /dev/null +++ b/scripts/setup-ga4.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env bash +set -euo pipefail + +# GA4 Setup Script for ShieldAI +# Two modes: +# 1. MANUAL: Step-by-step guide for GA web console (no credentials needed) +# 2. AUTOMATED: Creates property + stream via Admin API (requires GCP service account) +# +# Usage: +# ./scripts/setup-ga4.sh # Print manual instructions +# ./scripts/setup-ga4.sh --auto # Automated setup (needs GOOGLE_APPLICATION_CREDENTIALS) +# ./scripts/setup-ga4.sh --env-only # Just print what to put in .env + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +show_manual_guide() { + cat <<'GUIDE' +╔══════════════════════════════════════════════════════════════╗ +║ ShieldAI — Manual GA4 Setup Guide ║ +║ ~5 minutes in Google Analytics web console ║ +╚══════════════════════════════════════════════════════════════╝ + +STEP 1 — Create GA4 Property + 1. Go to https://analytics.google.com/ + 2. Admin → Create Property → "ShieldAI" + 3. Set reporting time zone, currency + 4. Click "Create" + +STEP 2 — Configure Data Stream + 1. In the new property: Admin → Data Streams → Add Stream → Web + 2. Website URL: https://shieldai.com + 3. Stream name: "ShieldAI Landing Page" + 4. Click "Create stream" + 5. Copy the Measurement ID (format: G-XXXXXXXXXX) + +STEP 3 — Create API Secret + 1. In the data stream details: Measurement Protocol API secrets → Create + 2. Nickname: "ShieldAI Backend" + 3. Copy the API Secret + +STEP 4 — Set Up Conversion Events + 1. In GA4: Admin → Conversions → New conversion event + 2. Create: "waitlist_signup" + 3. Create: "page_view" (auto-tracked by default) + 4. Optionally: "conversion" (for tracked conversions) + +STEP 5 — Configure Environment + Add to .env (or .env.prod for production): + GA4_MEASUREMENT_ID=G-XXXXXXXXXX + GA4_API_SECRET= + +STEP 6 — Verify + curl -X POST "https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=" \ + -H "Content-Type: application/json" \ + -d '{"client_id":"test-001","events":[{"name":"page_view"}]}' +GUIDE +} + +setup_automated() { + if [ -z "${GOOGLE_APPLICATION_CREDENTIALS:-}" ]; then + echo "ERROR: GOOGLE_APPLICATION_CREDENTIALS not set" + echo "Set it to the path of your GCP service account JSON key" + exit 1 + fi + + if ! command -v node &>/dev/null; then + echo "ERROR: node is required for automated setup" + exit 1 + fi + + echo "--- Automated GA4 Setup ---" + echo "Using service account: $GOOGLE_APPLICATION_CREDENTIALS" + + # Generate a setup script that uses the Google Admin API + cat > /tmp/setup-ga4-auto.mjs << 'SCRIPT' +import { google } from 'googleapis'; +import { readFileSync, writeFileSync } from 'fs'; + +async function main() { + const creds = JSON.parse(readFileSync(process.env.GOOGLE_APPLICATION_CREDENTIALS, 'utf-8')); + const auth = new google.auth.GoogleAuth({ + credentials: creds, + scopes: ['https://www.googleapis.com/auth/analytics.edit'], + }); + + const analyticsAdmin = google.analyticsadmin({ version: 'v1beta', auth }); + + // Step 1: Create GA4 property + console.log('Creating GA4 property...'); + const property = await analyticsAdmin.properties.create({ + requestBody: { + displayName: 'ShieldAI', + industryCategory: 'TECHNOLOGY', + timeZone: 'America/New_York', + currencyCode: 'USD', + parent: `accounts/${creds.account_id || '103950747'}`, // Replace with actual account ID + }, + }); + console.log(`Property created: ${property.data.name}`); + + // Step 2: Create web data stream + console.log('Creating web data stream...'); + const stream = await analyticsAdmin.properties.dataStreams.create({ + parent: property.data.name, + requestBody: { + type: 'WEB_DATA_STREAM', + displayName: 'ShieldAI Landing Page', + webStreamData: { + defaultUri: 'https://shieldai.com', + }, + }, + }); + console.log(`Data stream created: ${stream.data.name}`); + console.log(`Measurement ID: ${stream.data.webStreamData.measurementId}`); + + // Step 3: Create conversion events + console.log('Creating conversion events...'); + for (const event of ['waitlist_signup']) { + try { + await analyticsAdmin.properties.conversionEvents.create({ + parent: property.data.name, + requestBody: { eventName: event }, + }); + console.log(`Conversion event created: ${event}`); + } catch (e) { + console.log(`Conversion event ${event} may already exist: ${e.message}`); + } + } + + // Output results + const output = { + propertyId: property.data.name.replace('properties/', ''), + measurementId: stream.data.webStreamData.measurementId, + streamId: stream.data.name, + streamName: stream.data.displayName, + }; + writeFileSync('/tmp/ga4-setup-output.json', JSON.stringify(output, null, 2)); + console.log('\nResults saved to /tmp/ga4-setup-output.json'); + console.log(JSON.stringify(output, null, 2)); +} + +main().catch(console.error); +SCRIPT + + echo "" + echo "To run the automated setup:" + echo " 1. Update the account_id in the script above" + echo " 2. cd $PROJECT_DIR && node /tmp/setup-ga4-auto.mjs" + echo "" + echo "NOTE: You need to provide the Google Analytics account ID." + echo "Find it at: https://analytics.google.com/ → Admin → Account Settings" +} + +show_env_only() { + cat <<'ENV' +Required .env additions for ShieldAI analytics: + +GA4_MEASUREMENT_ID=G-XXXXXXXXXX # From GA4 data stream +GA4_API_SECRET= # From GA4 Measurement Protocol API secrets +MIXPANEL_TOKEN= # Mixpanel project token +MIXPANEL_API_SECRET= # Mixpanel project API secret +ENV +} + +case "${1:-}" in + --auto) + setup_automated + ;; + --env-only) + show_env_only + ;; + *) + show_manual_guide + ;; +esac