Skill: ga4-analytics-search-indexing (version 1.0.0)

--- name: ga4-analytics-nexus description: "Google Analytics 4, Search Console, and Indexing API toolkit with multi-property support. Analyze website traffic, page performance, user demographics, real-time visitors, search queries, and SEO metrics across multiple properties. Use when the user asks to: check site traffic for any property, analyze page views across multiple sites, see traffic sources, view user demographics, get real-time visitor data, check search console queries for any site, analyze SEO performance, request URL re-indexing, inspect index status, compare date ranges, check bounce rates, view conversion data, or get e-commerce revenue. Requires a Google Cloud service account with access to GA4 and Search Console properties." --- # GA4 Analytics Toolkit (Nexus Edition) Multi-property version supporting dynamic discovery of GA4 properties and Search Console sites. ## Setup Install dependencies: ```bash cd scripts && npm install ``` Configure credentials by setting the environment variable: ```bash export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json ``` Optional configuration: ``` GA4_DEFAULT_DATE_RANGE=30d ``` **Prerequisites**: - A Google Cloud project with the Analytics Data API, Analytics Admin API, Search Console API, and Indexing API enabled - A service account with access to your GA4 properties and Search Console sites - Service account must be added as Viewer to GA4 properties and have access to Search Console sites ## Quick Start First, discover available properties: ```typescript import { listGa4Properties, listSearchConsoleSites, siteOverview } from './scripts/src/index.js'; // List all accessible GA4 properties const properties = await listGa4Properties(); console.log(properties); // [{ propertyId: '123456789', displayName: 'My Site', accountId: '...' }, ...] // List all accessible Search Console sites const sites = await listSearchConsoleSites(); console.log(sites); // [{ siteUrl: 'https://example.com', permissionLevel: 'siteFullUser' }, ...] // Get overview for a specific property const overview = await siteOverview('123456789', '30d'); ``` ## Quick Reference | User says | Function to call | |-----------|-----------------| | "Show me my GA4 properties" | `listGa4Properties()` | | "Show me my Search Console sites" | `listSearchConsoleSites()` | | "Show me site traffic for property 123456789" | `siteOverview('123456789', '30d')` | | "What are my top search queries for example.com?" | `searchConsoleOverview('https://example.com', '30d')` | | "Who's on property 123456789 right now?" | `liveSnapshot('123456789')` | | "Reindex these URLs" | `reindexUrls(["https://example.com/page1", ...])` | | "Compare this month vs last month for property 123" | `compareDateRanges('123', {startDate: "30daysAgo", endDate: "today"}, {startDate: "60daysAgo", endDate: "31daysAgo"})` | | "What pages get the most traffic on property 123?" | `contentPerformance('123', '30d')` | Execute functions by importing from `scripts/src/index.ts`: ```typescript import { listGa4Properties, listSearchConsoleSites, siteOverview, searchConsoleOverview } from './scripts/src/index.js'; // List properties first const properties = await listGa4Properties(); // Then query specific property const overview = await siteOverview(properties[0].propertyId, '30d'); ``` Or run directly with tsx: ```bash npx tsx scripts/src/index.ts ``` ## Workflow Pattern Every analysis follows three phases: ### 1. Discover List available GA4 properties and Search Console sites to identify targets. ### 2. Analyze Run API functions with specific property IDs and site URLs. Each call hits the Google APIs and returns structured data. ### 3. Auto-Save All results automatically save as timestamped JSON files to `results/{category}/`. File naming pattern: `YYYYMMDD_HHMMSS__operation__extra_info.json` ### 4. Summarize After analysis, read the saved JSON files and create a markdown summary in `results/summaries/` with data tables, trends, and recommendations. ## Discovery Functions ### GA4 Property Discovery ```typescript const properties = await listGa4Properties(); // Returns: [{ propertyId: '123456789', displayName: 'My Site', accountId: '...' }, ...] ``` ### Search Console Site Discovery ```typescript const sites = await listSearchConsoleSites(); // Returns: [{ siteUrl: 'sc-domain:example.com', permissionLevel: 'siteFullUser' }, ...] ``` ## High-Level Functions ### GA4 Analytics | Function | Purpose | Parameters | |----------|---------|------------| | `siteOverview(propertyId, dateRange?)` | Comprehensive site snapshot | `propertyId`: string, `dateRange`: optional | | `trafficAnalysis(propertyId, dateRange?)` | Traffic deep-dive | `propertyId`: string, `dateRange`: optional | | `contentPerformance(propertyId, dateRange?)` | Top pages analysis | `propertyId`: string, `dateRange`: optional | | `userBehavior(propertyId, dateRange?)` | Engagement patterns | `propertyId`: string, `dateRange`: optional | | `compareDateRanges(propertyId, range1, range2)` | Period comparison | `propertyId`: string, ranges: DateRange objects | | `liveSnapshot(propertyId)` | Real-time data | `propertyId`: string | ### Search Console | Function | Purpose | Parameters | |----------|---------|------------| | `searchConsoleOverview(siteUrl, dateRange?)` | SEO snapshot | `siteUrl`: string (e.g., "https://example.com" or "sc-domain:example.com"), `dateRange`: optional | | `keywordAnalysis(siteUrl, dateRange?)` | Keyword deep-dive | `siteUrl`: string, `dateRange`: optional | | `seoPagePerformance(siteUrl, dateRange?)` | Page SEO metrics | `siteUrl`: string, `dateRange`: optional | ### Indexing | Function | Purpose | Parameters | |----------|---------|------------| | `reindexUrls(urls)` | Request re-indexing | `urls`: string[] | | `checkIndexStatus(siteUrl, urls)` | Check if URLs are indexed | `siteUrl`: string, `urls`: string[] | ### Utility | Function | Purpose | Parameters | |----------|---------|------------| | `getAvailableFields(propertyId)` | List all available GA4 dimensions and metrics | `propertyId`: string | ### Individual API Functions For granular control, import specific functions from the API modules: ```typescript import { runReport, getPageViews, getTopQueries } from './scripts/src/index.js'; // Run custom report for specific property const report = await runReport({ propertyId: '123456789', dimensions: ['pagePath'], metrics: ['screenPageViews'], dateRange: '7d' }); // Get Search Console data for specific site const queries = await getTopQueries('https://example.com', '30d'); ``` ## Date Ranges All functions accept flexible date range formats: | Format | Example | Description | |--------|---------|-------------| | Shorthand | `"7d"`, `"30d"`, `"90d"` | Days ago to today | | Explicit | `{startDate: "2024-01-01", endDate: "2024-01-31"}` | Specific dates | | GA4 relative | `{startDate: "30daysAgo", endDate: "today"}` | GA4 relative format | Default is `"30d"` (configurable via `GA4_DEFAULT_DATE_RANGE` env var). ## Results Storage Results auto-save to `results/` with this structure: ``` results/ ├── reports/ # GA4 standard reports ├── realtime/ # Real-time snapshots ├── searchconsole/ # Search Console data ├── indexing/ # Indexing API results └── summaries/ # Human-readable markdown summaries ``` ### Managing Results ```typescript import { listResults, loadResult, getLatestResult } from './scripts/src/index.js'; // List recent results const files = listResults('reports', 10); // Load a specific result const data = loadResult(files[0]); // Get most recent result for an operation const latest = getLatestResult('reports', 'site_overview'); ``` ## Common Dimensions and Metrics ### Dimensions `pagePath`, `pageTitle`, `sessionSource`, `sessionMedium`, `country`, `deviceCategory`, `browser`, `date`, `eventName`, `landingPage`, `newVsReturning` ### Metrics `screenPageViews`, `activeUsers`, `sessions`, `newUsers`, `bounceRate`, `averageSessionDuration`, `engagementRate`, `conversions`, `totalRevenue`, `eventCount` ## Tips 1. **Discover first** — Always start with `listGa4Properties()` and `listSearchConsoleSites()` to see what's available 2. **Multi-property** — This toolkit supports analyzing multiple GA4 properties and Search Console sites without reconfiguration 3. **Specify date ranges** — "last 7 days" or "last 90 days" gives different insights than the default 30 days 4. **Request summaries** — After pulling data, ask for a markdown summary with tables and insights 5. **Compare periods** — Use `compareDateRanges()` to spot trends (this month vs last month) 6. **Check real-time data** — `liveSnapshot()` shows who's on the site right now 7. **Combine GA4 + Search Console** — Traffic data plus search query data gives the full picture

path: _meta.json
content: ownerId: samiralibabic
slug: ga4-analytics-nexus
version: 1.0.0
publishedAt: 1769464446311
forkedFrom: adamkristopher/ga4-analytics

path: references/api-reference.md
content: # API Reference

Complete function reference for all GA4 Analytics Toolkit modules.

## Table of Contents

- [Reports API](#reports-api) (7 functions)
- [Realtime API](#realtime-api) (4 functions)
- [Metadata API](#metadata-api) (3 functions)
- [Search Console API](#search-console-api) (6 functions)
- [Indexing API](#indexing-api) (4 functions)
- [Bulk Lookup API](#bulk-lookup-api) (3 functions)
- [Storage](#storage) (4 functions)

---

## Reports API

Import: `from './api/reports.js'`

### `parseDateRange(range?)`

Parse shorthand date range (e.g., "7d", "30d") to GA4 date range format.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `range` | `string \| DateRange \| undefined` | Settings default | Date range to parse |

**Returns:** `DateRange` — `{startDate: string, endDate: string}`

### `runReport(options)`

Run a custom GA4 report with arbitrary dimensions and metrics.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `options.dimensions` | `string[]` | required | GA4 dimension names |
| `options.metrics` | `string[]` | required | GA4 metric names |
| `options.dateRange` | `string \| DateRange` | `"30d"` | Date range |
| `options.filters` | `Record<string, string>` | `undefined` | Dimension filters |
| `options.orderBy` | `string[]` | `undefined` | Sort order |
| `options.limit` | `number` | `undefined` | Row limit |
| `options.save` | `boolean` | `true` | Save results to JSON |

**Returns:** `Promise<ReportResponse>` — Rows with dimension and metric values.

### `getPageViews(dateRange?)`

Get page view data with paths, titles, users, and session duration.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `dateRange` | `string \| DateRange` | `"30d"` | Date range |

**Dimensions:** `pagePath`, `pageTitle`
**Metrics:** `screenPageViews`, `activeUsers`, `averageSessionDuration`

### `getTrafficSources(dateRange?)`

Get traffic source data by source, medium, and campaign.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `dateRange` | `string \| DateRange` | `"30d"` | Date range |

**Dimensions:** `sessionSource`, `sessionMedium`, `sessionCampaignName`
**Metrics:** `sessions`, `activeUsers`, `newUsers`, `bounceRate`

### `getUserDemographics(dateRange?)`

Get user demographic data by country, device, and browser.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `dateRange` | `string \| DateRange` | `"30d"` | Date range |

**Dimensions:** `country`, `deviceCategory`, `browser`
**Metrics:** `activeUsers`, `sessions`, `newUsers`

### `getEventCounts(dateRange?)`

Get event count data by event name.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `dateRange` | `string \| DateRange` | `"30d"` | Date range |

**Dimensions:** `eventName`
**Metrics:** `eventCount`, `eventCountPerUser`, `activeUsers`

### `getConversions(dateRange?)`

Get conversion data by event name and source.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `dateRange` | `string \| DateRange` | `"30d"` | Date range |

**Dimensions:** `eventName`, `sessionSource`
**Metrics:** `conversions`, `totalRevenue`

### `getEcommerceRevenue(dateRange?)`

Get e-commerce revenue data by date and transaction.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `dateRange` | `string \| DateRange` | `"30d"` | Date range |

**Dimensions:** `date`, `transactionId`
**Metrics:** `totalRevenue`, `ecommercePurchases`, `averagePurchaseRevenue`

---

## Realtime API

Import: `from './api/realtime.js'`

### `getActiveUsers(save?)`

Get current active users by screen/page name.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `save` | `boolean` | `true` | Save results to JSON |

**Returns:** `Promise<RealtimeResponse>` — Active users by `unifiedScreenName`.

### `getRealtimeEvents(save?)`

Get currently firing events.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `save` | `boolean` | `true` | Save results to JSON |

**Returns:** `Promise<RealtimeResponse>` — Event counts by `eventName`.

### `getRealtimePages(save?)`

Get currently viewed pages.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `save` | `boolean` | `true` | Save results to JSON |

**Returns:** `Promise<RealtimeResponse>` — Page views by `unifiedScreenName`.

### `getRealtimeSources(save?)`

Get current traffic sources.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `save` | `boolean` | `true` | Save results to JSON |

**Returns:** `Promise<RealtimeResponse>` — Active users by `firstUserSource` and `firstUserMedium`.

---

## Metadata API

Import: `from './api/metadata.js'`

### `getAvailableDimensions(save?)`

Get all available dimensions for the GA4 property.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `save` | `boolean` | `true` | Save results to JSON |

**Returns:** `Promise<MetadataResponse>` — Array of `{apiName, uiName, description}`.

### `getAvailableMetrics(save?)`

Get all available metrics for the GA4 property.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `save` | `boolean` | `true` | Save results to JSON |

**Returns:** `Promise<MetadataResponse>` — Array of `{apiName, uiName, description}`.

### `getPropertyMetadata(save?)`

Get full property metadata (dimensions + metrics combined).

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `save` | `boolean` | `true` | Save results to JSON |

**Returns:** `Promise<MetadataResponse>` — Full metadata response.

---

## Search Console API

Import: `from './api/searchConsole.js'`

### `querySearchAnalytics(options)`

Run a custom Search Console analytics query.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `options.dimensions` | `string[]` | `["query"]` | Dimensions: `query`, `page`, `device`, `country`, `searchAppearance` |
| `options.dateRange` | `string \| SearchConsoleDateRange` | `"30d"` | Date range |
| `options.rowLimit` | `number` | `1000` | Max rows |
| `options.startRow` | `number` | `0` | Pagination offset |
| `options.save` | `boolean` | `true` | Save results to JSON |

**Returns:** `Promise<SearchAnalyticsResponse>` — Rows with `{keys, clicks, impressions, ctr, position}`.

### `getTopQueries(dateRange?)`

Get top 100 search queries by clicks.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `dateRange` | `string \| SearchConsoleDateRange` | `"30d"` | Date range |

**Returns:** `Promise<SearchAnalyticsResponse>`

### `getTopPages(dateRange?)`

Get top 100 pages by search impressions.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `dateRange` | `string \| SearchConsoleDateRange` | `"30d"` | Date range |

**Returns:** `Promise<SearchAnalyticsResponse>`

### `getDevicePerformance(dateRange?)`

Get search performance breakdown by device type (desktop, mobile, tablet).

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `dateRange` | `string \| SearchConsoleDateRange` | `"30d"` | Date range |

**Returns:** `Promise<SearchAnalyticsResponse>`

### `getCountryPerformance(dateRange?)`

Get search performance by country (top 50).

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `dateRange` | `string \| SearchConsoleDateRange` | `"30d"` | Date range |

**Returns:** `Promise<SearchAnalyticsResponse>`

### `getSearchAppearance(dateRange?)`

Get search appearance data (rich results, AMP, etc.).

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `dateRange` | `string \| SearchConsoleDateRange` | `"30d"` | Date range |

**Returns:** `Promise<SearchAnalyticsResponse>`

---

## Indexing API

Import: `from './api/indexing.js'`

### `requestIndexing(url, options?)`

Request Google to re-crawl a single URL.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `url` | `string` | required | Full URL to request indexing for |
| `options.save` | `boolean` | `true` | Save results to JSON |

**Returns:** `Promise<UrlNotificationResult>` — `{url, type: 'URL_UPDATED', notifyTime}`.

### `requestIndexingBatch(urls, options?)`

Request re-crawling for multiple URLs (processed sequentially to avoid rate limits).

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `urls` | `string[]` | required | Array of URLs |
| `options.save` | `boolean` | `true` | Save results to JSON |

**Returns:** `Promise<UrlNotificationResult[]>`

### `removeFromIndex(url, options?)`

Request URL removal from Google's index.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `url` | `string` | required | URL to remove |
| `options.save` | `boolean` | `true` | Save results to JSON |

**Returns:** `Promise<UrlNotificationResult>` — `{url, type: 'URL_DELETED', notifyTime}`.

### `inspectUrl(url, options?)`

Check a URL's index status, mobile usability, and rich results.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `url` | `string` | required | URL to inspect |
| `options.save` | `boolean` | `true` | Save results to JSON |

**Returns:** `Promise<UrlInspectionResult>` — Contains `indexStatus.verdict` ('PASS' | 'FAIL' | 'NEUTRAL'), `coverageState`, `lastCrawlTime`, `mobileUsability`, `richResults`.

---

## Bulk Lookup API

Import: `from './api/bulk-lookup.js'`

### `normalizeUrls(urls)`

Normalize page paths: trim whitespace, add leading slash.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `urls` | `string[]` | required | Array of page paths |

**Returns:** `string[]` — Normalized paths.

### `buildUrlFilter(urls)`

Build a GA4 dimension filter expression for a list of page paths.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `urls` | `string[]` | required | Normalized page paths |

**Returns:** `DimensionFilterExpression | null`

### `getMetricsForUrls(urls, options?)`

Get GA4 metrics for specific page paths (bulk lookup).

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `urls` | `string[]` | required | Page paths (e.g., `["/pricing", "/about"]`) |
| `options.dateRange` | `string \| DateRange` | `"30d"` | Date range |
| `options.metrics` | `string[]` | `["screenPageViews", "activeUsers", "averageSessionDuration", "bounceRate", "engagementRate"]` | Metrics to retrieve |
| `options.save` | `boolean` | `true` | Save results to JSON |

**Returns:** `Promise<ReportResponse>` — Metrics for each URL.

---

## Storage

Import: `from './core/storage.js'`

### `saveResult<T>(data, category, operation, extraInfo?)`

Save result data to a timestamped JSON file with metadata wrapper.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `data` | `T` | required | Data to save |
| `category` | `string` | required | Category directory (e.g., `"reports"`, `"realtime"`) |
| `operation` | `string` | required | Operation name (e.g., `"page_views"`) |
| `extraInfo` | `string` | `undefined` | Optional extra info for filename |

**Returns:** `string` — Full path to the saved file.

### `loadResult<T>(filepath)`

Load a previously saved result from a JSON file.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `filepath` | `string` | required | Path to the JSON file |

**Returns:** `SavedResult<T> | null`

### `listResults(category, limit?)`

List saved result files for a category, sorted newest first.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `category` | `string` | required | Category to list |
| `limit` | `number` | `undefined` | Max results to return |

**Returns:** `string[]` — Array of file paths.

### `getLatestResult<T>(category, operation?)`

Get the most recent result for a category, optionally filtered by operation.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `category` | `string` | required | Category to search |
| `operation` | `string` | `undefined` | Filter by operation name |

**Returns:** `SavedResult<T> | null`


path: scripts/setup.sh
content: #!/bin/bash
cd "$(dirname "$0")" && npm install


path: scripts/package.json
content: {
  "name": "ga4-analytics-nexus",
  "version": "1.0.0",
  "type": "module",
  "description": "Multi-property GA4 Analytics toolkit with discovery support",
  "dependencies": {
    "@google-analytics/admin": "^7.0.0",
    "@google-analytics/data": "^4.9.0",
    "@googleapis/indexing": "^6.0.1",
    "@googleapis/searchconsole": "^6.0.1",
    "dotenv": "^16.4.7",
    "googleapis": "^170.0.0",
    "tsx": "^4.19.2",
    "typescript": "^5.7.2"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}


path: scripts/tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "tests"]
}


path: scripts/src/core/storage.ts
content: /**
 * Storage Module - Auto-save results to JSON files with metadata
 */

import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from 'fs';
import { join } from 'path';
import { getSettings } from '../config/settings.js';

/**
 * Metadata wrapper for saved results
 */
export interface ResultMetadata {
  savedAt: string;
  category: string;
  operation: string;
  propertyId: string;
  extraInfo?: string;
}

/**
 * Wrapped result with metadata
 */
export interface SavedResult<T = unknown> {
  metadata: ResultMetadata;
  data: T;
}

/**
 * Generate timestamp string for filenames: YYYYMMDD_HHMMSS
 */
function getTimestamp(): string {
  const now = new Date();
  const year = now.getFullYear();
  const month = String(now.getMonth() + 1).padStart(2, '0');
  const day = String(now.getDate()).padStart(2, '0');
  const hours = String(now.getHours()).padStart(2, '0');
  const minutes = String(now.getMinutes()).padStart(2, '0');
  const seconds = String(now.getSeconds()).padStart(2, '0');
  return `${year}${month}${day}_${hours}${minutes}${seconds}`;
}

/**
 * Sanitize string for use in filename
 */
function sanitizeFilename(str: string): string {
  return str.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
}

/**
 * Save result data to a JSON file with metadata wrapper
 *
 * @param data - The data to save
 * @param category - Category directory (e.g., 'reports', 'realtime')
 * @param operation - Operation name (e.g., 'page_views', 'traffic_sources')
 * @param extraInfo - Optional extra info for filename
 * @returns The full path to the saved file
 */
export function saveResult<T>(
  data: T,
  category: string,
  operation: string,
  extraInfo?: string
): string {
  const settings = getSettings();
  const categoryDir = join(settings.resultsDir, category);

  // Ensure category directory exists
  if (!existsSync(categoryDir)) {
    mkdirSync(categoryDir, { recursive: true });
  }

  // Build filename
  const timestamp = getTimestamp();
  const sanitizedOperation = sanitizeFilename(operation);
  const sanitizedExtra = extraInfo ? `__${sanitizeFilename(extraInfo)}` : '';
  const filename = `${timestamp}__${sanitizedOperation}${sanitizedExtra}.json`;
  const filepath = join(categoryDir, filename);

  // Build wrapped result
  const result: SavedResult<T> = {
    metadata: {
      savedAt: new Date().toISOString(),
      category,
      operation,
      propertyId: settings.propertyId,
      ...(extraInfo && { extraInfo }),
    },
    data,
  };

  // Write to file
  writeFileSync(filepath, JSON.stringify(result, null, 2), 'utf-8');

  return filepath;
}

/**
 * Load a saved result from a JSON file
 *
 * @param filepath - Path to the JSON file
 * @returns The parsed result or null if file doesn't exist
 */
export function loadResult<T = unknown>(filepath: string): SavedResult<T> | null {
  if (!existsSync(filepath)) {
    return null;
  }

  try {
    const content = readFileSync(filepath, 'utf-8');
    return JSON.parse(content) as SavedResult<T>;
  } catch {
    return null;
  }
}

/**
 * List saved result files for a category
 *
 * @param category - Category to list results for
 * @param limit - Maximum number of results to return
 * @returns Array of file paths, sorted by date descending (newest first)
 */
export function listResults(category: string, limit?: number): string[] {
  const settings = getSettings();
  const categoryDir = join(settings.resultsDir, category);

  if (!existsSync(categoryDir)) {
    return [];
  }

  const files = readdirSync(categoryDir)
    .filter(f => f.endsWith('.json'))
    .map(f => join(categoryDir, f))
    .sort((a, b) => {
      // Sort by filename (which starts with timestamp) descending
      const nameA = a.split('/').pop() || '';
      const nameB = b.split('/').pop() || '';
      return nameB.localeCompare(nameA);
    });

  if (limit !== undefined) {
    return files.slice(0, limit);
  }

  return files;
}

/**
 * Get the most recent result for a category/operation
 *
 * @param category - Category to search
 * @param operation - Optional operation to filter by
 * @returns The most recent result or null
 */
export function getLatestResult<T = unknown>(
  category: string,
  operation?: string
): SavedResult<T> | null {
  let files = listResults(category);

  if (operation) {
    const sanitized = sanitizeFilename(operation);
    files = files.filter(f => f.includes(`__${sanitized}`));
  }

  if (files.length === 0) {
    return null;
  }

  return loadResult<T>(files[0]);
}


path: scripts/src/core/client.ts
content: /**
 * GA4 API Client - Multi-property version with ADC support
 * Also includes Search Console and Indexing API clients
 */

import { BetaAnalyticsDataClient } from '@google-analytics/data';
import { AnalyticsAdminServiceClient } from '@google-analytics/admin';
import { searchconsole } from '@googleapis/searchconsole';
import { indexing } from '@googleapis/indexing';
import { google } from 'googleapis';
import { getSettings, validateSettings } from '../config/settings.js';

// Singleton client instances
let clientInstance: BetaAnalyticsDataClient | null = null;
let adminClientInstance: AnalyticsAdminServiceClient | null = null;
let searchConsoleClientInstance: ReturnType<typeof searchconsole> | null = null;
let indexingClientInstance: ReturnType<typeof indexing> | null = null;

/**
 * Get the Google Auth client configuration
 * Uses GOOGLE_APPLICATION_CREDENTIALS if set, otherwise falls back to ADC
 */
function getAuthConfig() {
  const settings = getSettings();
  
  if (settings.googleApplicationCredentials) {
    // Use explicit credentials file
    return {
      keyFile: settings.googleApplicationCredentials,
      scopes: [
        'https://www.googleapis.com/auth/analytics.readonly',
        'https://www.googleapis.com/auth/webmasters.readonly',
        'https://www.googleapis.com/auth/indexing',
      ],
    };
  }
  
  // Fall back to Application Default Credentials
  return {
    scopes: [
      'https://www.googleapis.com/auth/analytics.readonly',
      'https://www.googleapis.com/auth/webmasters.readonly',
      'https://www.googleapis.com/auth/indexing',
    ],
  };
}

/**
 * Get the GA4 Analytics Data API client (singleton)
 *
 * @returns The BetaAnalyticsDataClient instance
 * @throws Error if credentials are invalid
 */
export function getClient(): BetaAnalyticsDataClient {
  if (clientInstance) {
    return clientInstance;
  }

  const validation = validateSettings();
  if (!validation.valid) {
    throw new Error(`Invalid settings: ${validation.errors.join(', ')}`);
  }

  const authConfig = getAuthConfig();
  clientInstance = new BetaAnalyticsDataClient(authConfig);

  return clientInstance;
}

/**
 * Get the GA4 Admin API client for property discovery (singleton)
 *
 * @returns The AnalyticsAdminServiceClient instance
 */
export function getAdminClient(): AnalyticsAdminServiceClient {
  if (adminClientInstance) {
    return adminClientInstance;
  }

  const authConfig = getAuthConfig();
  adminClientInstance = new AnalyticsAdminServiceClient(authConfig);

  return adminClientInstance;
}

/**
 * Format a property ID for API calls
 *
 * @param propertyId - The GA4 property ID (numeric)
 * @returns Property ID with "properties/" prefix
 */
export function formatPropertyId(propertyId: string): string {
  return `properties/${propertyId}`;
}

/**
 * Reset the client singleton (useful for testing)
 */
export function resetClient(): void {
  clientInstance = null;
  adminClientInstance = null;
  searchConsoleClientInstance = null;
  indexingClientInstance = null;
}

/**
 * Get Google Auth client for Search Console and Indexing APIs
 */
function getGoogleAuth() {
  const settings = getSettings();
  
  if (settings.googleApplicationCredentials) {
    return new google.auth.GoogleAuth({
      keyFile: settings.googleApplicationCredentials,
      scopes: [
        'https://www.googleapis.com/auth/webmasters.readonly',
        'https://www.googleapis.com/auth/indexing',
      ],
    });
  }
  
  return new google.auth.GoogleAuth({
    scopes: [
      'https://www.googleapis.com/auth/webmasters.readonly',
      'https://www.googleapis.com/auth/indexing',
    ],
  });
}

/**
 * Get the Search Console API client (singleton)
 *
 * @returns The Search Console client instance
 * @throws Error if credentials are invalid
 */
export function getSearchConsoleClient(): ReturnType<typeof searchconsole> {
  if (searchConsoleClientInstance) {
    return searchConsoleClientInstance;
  }

  const auth = getGoogleAuth();
  searchConsoleClientInstance = searchconsole({ version: 'v1', auth });

  return searchConsoleClientInstance;
}

/**
 * Get the Indexing API client (singleton)
 *
 * @returns The Indexing client instance
 * @throws Error if credentials are invalid
 */
export function getIndexingClient(): ReturnType<typeof indexing> {
  if (indexingClientInstance) {
    return indexingClientInstance;
  }

  const auth = getGoogleAuth();
  indexingClientInstance = indexing({ version: 'v3', auth });

  return indexingClientInstance;
}

/**
 * List all GA4 properties accessible to the service account
 *
 * @returns Array of properties with their IDs and display names
 */
export async function listGa4Properties(): Promise<Array<{ propertyId: string; displayName: string; accountId: string }>> {
  const adminClient = getAdminClient();
  const [accounts] = await adminClient.listAccounts();
  
  const properties: Array<{ propertyId: string; displayName: string; accountId: string }> = [];
  
  for (const account of accounts || []) {
    if (!account.name) continue;
    const accountId = account.name.split('/')[1];
    
    try {
      const [accountProperties] = await adminClient.listProperties({
        filter: `parent:accounts/${accountId}`,
      });
      
      for (const property of accountProperties || []) {
        if (property.name) {
          const propertyId = property.name.split('/')[1];
          properties.push({
            propertyId,
            displayName: property.displayName || 'Unnamed Property',
            accountId,
          });
        }
      }
    } catch (error) {
      // Skip accounts we can't access
      console.warn(`Could not list properties for account ${accountId}: ${error}`);
    }
  }
  
  return properties;
}

/**
 * List all Search Console properties accessible to the service account
 *
 * @returns Array of site URLs
 */
export async function listSearchConsoleSites(): Promise<Array<{ siteUrl: string; permissionLevel: string }>> {
  const client = getSearchConsoleClient();
  const response = await client.sites.list();
  
  return (response.data.siteEntry || []).map((entry: any) => ({
    siteUrl: entry.siteUrl || '',
    permissionLevel: entry.permissionLevel || 'unknown',
  }));
}


path: scripts/src/config/settings.ts
content: /**
 * Settings Module - Environment configuration for GA4 API
 * Multi-property version - supports GOOGLE_APPLICATION_CREDENTIALS
 */

import { config } from 'dotenv';
import { join } from 'path';

// Load .env file from current working directory
config();

/**
 * Settings interface for GA4 API configuration
 */
export interface Settings {
  /** Default date range for reports (e.g., "30d", "7d") */
  defaultDateRange: string;
  /** Directory path for storing results */
  resultsDir: string;
  /** Path to Google Application Credentials JSON (optional - uses ADC if not set) */
  googleApplicationCredentials?: string;
}

/**
 * Validation result from validateSettings()
 */
export interface ValidationResult {
  valid: boolean;
  errors: string[];
}

/**
 * Get current settings from environment variables
 */
export function getSettings(): Settings {
  return {
    defaultDateRange: process.env.GA4_DEFAULT_DATE_RANGE || '30d',
    resultsDir: join(process.cwd(), 'results'),
    googleApplicationCredentials: process.env.GOOGLE_APPLICATION_CREDENTIALS,
  };
}

/**
 * Validate that basic settings are present
 * Note: Property IDs and site URLs are now passed as parameters, not env vars
 */
export function validateSettings(): ValidationResult {
  const settings = getSettings();
  const errors: string[] = [];

  // No required env vars anymore - GOOGLE_APPLICATION_CREDENTIALS is optional
  // (falls back to Application Default Credentials)

  return {
    valid: errors.length === 0,
    errors,
  };
}


path: scripts/src/api/bulk-lookup.ts
content: /**
 * Bulk URL Lookup - Get GA4 metrics for specific page paths
 *
 * This module provides a convenient way to look up analytics data
 * for a list of specific URLs, similar to a bulk URL lookup field.
 * Multi-property version - propertyId passed as parameter
 */

import { getClient, formatPropertyId } from '../core/client.js';
import { saveResult } from '../core/storage.js';
import { getSettings } from '../config/settings.js';
import type { ReportResponse, DateRange } from './reports.js';

/**
 * Options for bulk URL lookup
 */
export interface BulkLookupOptions {
  /** Date range (e.g., "7d", "30d") or explicit dates */
  dateRange?: string | DateRange;
  /** Custom metrics to retrieve (defaults to standard page metrics) */
  metrics?: string[];
  /** Whether to save results to file (default: true) */
  save?: boolean;
}

/**
 * Dimension filter expression for GA4 API
 */
export interface DimensionFilterExpression {
  filter: {
    fieldName: string;
    inListFilter?: {
      values: string[];
      caseSensitive?: boolean;
    };
    stringFilter?: {
      matchType: string;
      value: string;
      caseSensitive?: boolean;
    };
  };
}

/**
 * Default metrics for bulk URL lookup
 */
const DEFAULT_METRICS = [
  'screenPageViews',
  'activeUsers',
  'averageSessionDuration',
  'bounceRate',
  'engagementRate',
];

/**
 * Normalize URLs to ensure consistent format
 *
 * - Trims whitespace
 * - Adds leading slash if missing
 * - Preserves trailing slashes
 *
 * @param urls - Array of URLs to normalize
 * @returns Normalized URL array
 */
export function normalizeUrls(urls: string[]): string[] {
  return urls.map(url => {
    // Trim whitespace
    let normalized = url.trim();

    // Add leading slash if missing
    if (!normalized.startsWith('/')) {
      normalized = '/' + normalized;
    }

    return normalized;
  });
}

/**
 * Build a dimension filter expression for the given URLs
 *
 * @param urls - Array of page paths to filter by
 * @returns Filter expression or null if no URLs provided
 */
export function buildUrlFilter(urls: string[]): DimensionFilterExpression | null {
  if (urls.length === 0) {
    return null;
  }

  return {
    filter: {
      fieldName: 'pagePath',
      inListFilter: {
        values: urls,
        caseSensitive: false,
      },
    },
  };
}

/**
 * Parse shorthand date range (e.g., "7d", "30d") to GA4 date range format
 */
function parseDateRange(range: string | DateRange | undefined): DateRange {
  if (!range) {
    const settings = getSettings();
    range = settings.defaultDateRange;
  }

  if (typeof range === 'object') {
    return range;
  }

  // Parse shorthand like "7d", "30d", "90d"
  const match = range.match(/^(\d+)d$/);
  if (match) {
    const days = parseInt(match[1], 10);
    return {
      startDate: `${days}daysAgo`,
      endDate: 'today',
    };
  }

  // Default to 30 days
  return {
    startDate: '30daysAgo',
    endDate: 'today',
  };
}

/**
 * Get GA4 metrics for specific page paths (bulk URL lookup)
 *
 * @param propertyId - GA4 Property ID (numeric, e.g., "123456789")
 * @param urls - Array of page paths to look up (e.g., ['/pricing', '/about'])
 * @param options - Optional configuration
 * @returns Report response with metrics for the specified URLs
 *
 * @example
 * ```typescript
 * // Get metrics for specific pages
 * const result = await getMetricsForUrls('123456789', ['/pricing', '/about', '/blog']);
 *
 * // With custom date range and metrics
 * const result = await getMetricsForUrls('123456789', ['/pricing'], {
 *   dateRange: '7d',
 *   metrics: ['sessions', 'bounceRate'],
 * });
 * ```
 */
export async function getMetricsForUrls(
  propertyId: string,
  urls: string[],
  options: BulkLookupOptions = {}
): Promise<ReportResponse> {
  const { dateRange, metrics = DEFAULT_METRICS, save = true } = options;

  // Normalize URLs
  const normalizedUrls = normalizeUrls(urls);

  // Handle empty URL array
  if (normalizedUrls.length === 0) {
    return {
      rows: [],
      rowCount: 0,
    };
  }

  // Build filter
  const dimensionFilter = buildUrlFilter(normalizedUrls);

  // Get client and property
  const client = getClient();
  const property = formatPropertyId(propertyId);
  const parsedDateRange = parseDateRange(dateRange);

  // Build and execute request
  const request: any = {
    property,
    dateRanges: [parsedDateRange],
    dimensions: [{ name: 'pagePath' }, { name: 'pageTitle' }],
    metrics: metrics.map(name => ({ name })),
    dimensionFilter,
  };

  const [response] = await client.runReport(request);

  // Save results if requested
  if (save) {
    const dateStr = typeof dateRange === 'string' ? dateRange : 'custom';
    saveResult(response, 'reports', 'bulk_url_lookup', dateStr);
  }

  return response as ReportResponse;
}


path: scripts/src/api/indexing.ts
content: /**
 * Indexing API - Request re-indexing and URL inspection
 * Multi-property version - siteUrl passed as parameter for inspection
 */

import { getIndexingClient, getSearchConsoleClient } from '../core/client.js';
import { saveResult } from '../core/storage.js';

/**
 * Indexing request options
 */
export interface IndexingOptions {
  save?: boolean;
}

/**
 * URL inspection options (requires siteUrl)
 */
export interface UrlInspectionOptions extends IndexingOptions {
  /** Search Console site URL (e.g., "https://example.com" or "sc-domain:example.com") */
  siteUrl: string;
}

/**
 * URL notification result
 */
export interface UrlNotificationResult {
  url: string;
  type: 'URL_UPDATED' | 'URL_DELETED';
  notifyTime: string;
}

/**
 * URL inspection result
 */
export interface UrlInspectionResult {
  inspectionResultLink?: string;
  indexStatus: {
    verdict: 'PASS' | 'FAIL' | 'NEUTRAL';
    coverageState: string;
    robotsTxtState?: string;
    indexingState?: string;
    lastCrawlTime?: string;
    pageFetchState?: string;
    googleCanonical?: string;
    userCanonical?: string;
    crawledAs?: string;
  };
  mobileUsability?: {
    verdict: string;
    issues?: unknown[];
  };
  richResults?: {
    verdict: string;
    detectedItems?: unknown[];
  };
}

/**
 * Request indexing for a single URL (notify Google to re-crawl)
 *
 * @param url - The URL to request indexing for
 * @param options - Optional settings (save to file, etc.)
 * @returns Notification result with timestamp
 */
export async function requestIndexing(url: string, options: IndexingOptions = {}): Promise<UrlNotificationResult> {
  const { save = true } = options;

  const client = getIndexingClient();

  const response = await client.urlNotifications.publish({
    requestBody: {
      url,
      type: 'URL_UPDATED',
    },
  });

  const result: UrlNotificationResult = {
    url: response.data.urlNotificationMetadata?.url || url,
    type: 'URL_UPDATED',
    notifyTime: response.data.urlNotificationMetadata?.latestUpdate?.notifyTime || new Date().toISOString(),
  };

  if (save) {
    saveResult(result, 'indexing', 'request_indexing');
  }

  return result;
}

/**
 * Request indexing for multiple URLs
 *
 * @param urls - Array of URLs to request indexing for
 * @param options - Optional settings
 * @returns Array of notification results
 */
export async function requestIndexingBatch(urls: string[], options: IndexingOptions = {}): Promise<UrlNotificationResult[]> {
  const { save = true } = options;

  const results: UrlNotificationResult[] = [];

  // Process URLs sequentially to avoid rate limiting
  for (const url of urls) {
    const result = await requestIndexing(url, { save: false });
    results.push(result);
  }

  if (save) {
    saveResult(results, 'indexing', 'batch_indexing');
  }

  return results;
}

/**
 * Request URL removal from index
 *
 * @param url - The URL to request removal for
 * @param options - Optional settings
 * @returns Notification result
 */
export async function removeFromIndex(url: string, options: IndexingOptions = {}): Promise<UrlNotificationResult> {
  const { save = true } = options;

  const client = getIndexingClient();

  const response = await client.urlNotifications.publish({
    requestBody: {
      url,
      type: 'URL_DELETED',
    },
  });

  const result: UrlNotificationResult = {
    url: response.data.urlNotificationMetadata?.url || url,
    type: 'URL_DELETED',
    notifyTime: response.data.urlNotificationMetadata?.latestRemove?.notifyTime || new Date().toISOString(),
  };

  if (save) {
    saveResult(result, 'indexing', 'remove_from_index');
  }

  return result;
}

/**
 * Inspect a URL to check its index status
 *
 * @param url - The URL to inspect
 * @param options - Requires siteUrl
 * @returns URL inspection result with index status
 */
export async function inspectUrl(url: string, options: UrlInspectionOptions): Promise<UrlInspectionResult> {
  const { siteUrl, save = true } = options;

  const client = getSearchConsoleClient();

  const response = await client.urlInspection.index.inspect({
    requestBody: {
      inspectionUrl: url,
      siteUrl,
    },
  });

  const inspectionResult = response.data.inspectionResult;

  const result: UrlInspectionResult = {
    inspectionResultLink: inspectionResult?.inspectionResultLink || undefined,
    indexStatus: {
      verdict: (inspectionResult?.indexStatusResult?.verdict as 'PASS' | 'FAIL' | 'NEUTRAL') || 'NEUTRAL',
      coverageState: inspectionResult?.indexStatusResult?.coverageState || 'Unknown',
      robotsTxtState: inspectionResult?.indexStatusResult?.robotsTxtState || undefined,
      indexingState: inspectionResult?.indexStatusResult?.indexingState || undefined,
      lastCrawlTime: inspectionResult?.indexStatusResult?.lastCrawlTime || undefined,
      pageFetchState: inspectionResult?.indexStatusResult?.pageFetchState || undefined,
      googleCanonical: inspectionResult?.indexStatusResult?.googleCanonical || undefined,
      userCanonical: inspectionResult?.indexStatusResult?.userCanonical || undefined,
      crawledAs: inspectionResult?.indexStatusResult?.crawledAs || undefined,
    },
    mobileUsability: inspectionResult?.mobileUsabilityResult
      ? {
          verdict: inspectionResult.mobileUsabilityResult.verdict || 'NEUTRAL',
          issues: inspectionResult.mobileUsabilityResult.issues || [],
        }
      : undefined,
    richResults: inspectionResult?.richResultsResult
      ? {
          verdict: inspectionResult.richResultsResult.verdict || 'NEUTRAL',
          detectedItems: inspectionResult.richResultsResult.detectedItems || [],
        }
      : undefined,
  };

  if (save) {
    saveResult(result, 'indexing', 'url_inspection');
  }

  return result;
}


path: scripts/src/api/reports.ts
content: /**
 * Reports API - Standard GA4 report generation
 * Multi-property version - propertyId passed as parameter
 */

import { getClient, formatPropertyId } from '../core/client.js';
import { saveResult } from '../core/storage.js';
import { getSettings } from '../config/settings.js';

/**
 * Date range configuration
 */
export interface DateRange {
  startDate: string;
  endDate: string;
}

/**
 * Report options
 */
export interface ReportOptions {
  /** GA4 Property ID (numeric, e.g., "123456789") */
  propertyId: string;
  dimensions: string[];
  metrics: string[];
  dateRange?: string | DateRange;
  filters?: Record<string, string>;
  orderBy?: string[];
  limit?: number;
  save?: boolean;
}

/**
 * Report response structure
 */
export interface ReportResponse {
  dimensionHeaders?: Array<{ name: string }>;
  metricHeaders?: Array<{ name: string }>;
  rows?: Array<{
    dimensionValues: Array<{ value: string }>;
    metricValues: Array<{ value: string }>;
  }>;
  rowCount?: number;
  metadata?: Record<string, unknown>;
}

/**
 * Parse shorthand date range (e.g., "7d", "30d") to GA4 date range format
 */
export function parseDateRange(range: string | DateRange | undefined): DateRange {
  if (!range) {
    const settings = getSettings();
    range = settings.defaultDateRange;
  }

  if (typeof range === 'object') {
    return range;
  }

  // Parse shorthand like "7d", "30d", "90d"
  const match = range.match(/^(\d+)d$/);
  if (match) {
    const days = parseInt(match[1], 10);
    return {
      startDate: `${days}daysAgo`,
      endDate: 'today',
    };
  }

  // Default to 30 days
  return {
    startDate: '30daysAgo',
    endDate: 'today',
  };
}

/**
 * Run a custom GA4 report
 */
export async function runReport(options: ReportOptions): Promise<ReportResponse> {
  const {
    propertyId,
    dimensions,
    metrics,
    dateRange,
    filters,
    orderBy,
    limit,
    save = true,
  } = options;

  const client = getClient();
  const property = formatPropertyId(propertyId);
  const parsedDateRange = parseDateRange(dateRange);

  const request: any = {
    property,
    dateRanges: [parsedDateRange],
    dimensions: dimensions.map(name => ({ name })),
    metrics: metrics.map(name => ({ name })),
    ...(limit && { limit }),
  };

  const [response] = await client.runReport(request);

  if (save) {
    const operation = dimensions.join('_') || 'custom';
    const extra = typeof dateRange === 'string' ? dateRange : undefined;
    saveResult(response, 'reports', operation, extra);
  }

  return response as ReportResponse;
}

/**
 * Get page view data
 */
export async function getPageViews(propertyId: string, dateRange?: string | DateRange): Promise<ReportResponse> {
  return runReport({
    propertyId,
    dimensions: ['pagePath', 'pageTitle'],
    metrics: ['screenPageViews', 'activeUsers', 'averageSessionDuration'],
    dateRange,
  });
}

/**
 * Get traffic source data
 */
export async function getTrafficSources(propertyId: string, dateRange?: string | DateRange): Promise<ReportResponse> {
  return runReport({
    propertyId,
    dimensions: ['sessionSource', 'sessionMedium', 'sessionCampaignName'],
    metrics: ['sessions', 'activeUsers', 'newUsers', 'bounceRate'],
    dateRange,
  });
}

/**
 * Get user demographic data (country, device, browser)
 */
export async function getUserDemographics(propertyId: string, dateRange?: string | DateRange): Promise<ReportResponse> {
  return runReport({
    propertyId,
    dimensions: ['country', 'deviceCategory', 'browser'],
    metrics: ['activeUsers', 'sessions', 'newUsers'],
    dateRange,
  });
}

/**
 * Get event count data
 */
export async function getEventCounts(propertyId: string, dateRange?: string | DateRange): Promise<ReportResponse> {
  return runReport({
    propertyId,
    dimensions: ['eventName'],
    metrics: ['eventCount', 'eventCountPerUser', 'activeUsers'],
    dateRange,
  });
}

/**
 * Get conversion data
 */
export async function getConversions(propertyId: string, dateRange?: string | DateRange): Promise<ReportResponse> {
  return runReport({
    propertyId,
    dimensions: ['eventName', 'sessionSource'],
    metrics: ['conversions', 'totalRevenue'],
    dateRange,
  });
}

/**
 * Get e-commerce revenue data
 */
export async function getEcommerceRevenue(propertyId: string, dateRange?: string | DateRange): Promise<ReportResponse> {
  return runReport({
    propertyId,
    dimensions: ['date', 'transactionId'],
    metrics: ['totalRevenue', 'ecommercePurchases', 'averagePurchaseRevenue'],
    dateRange,
  });
}


path: scripts/src/api/realtime.ts
content: /**
 * Realtime API - Live GA4 data
 * Multi-property version - propertyId passed as parameter
 */

import { getClient, formatPropertyId } from '../core/client.js';
import { saveResult } from '../core/storage.js';

/**
 * Realtime report response structure
 */
export interface RealtimeResponse {
  dimensionHeaders?: Array<{ name: string }>;
  metricHeaders?: Array<{ name: string }>;
  rows?: Array<{
    dimensionValues: Array<{ value: string }>;
    metricValues: Array<{ value: string }>;
  }>;
  rowCount?: number;
}

/**
 * Get current active users
 */
export async function getActiveUsers(propertyId: string, save = true): Promise<RealtimeResponse> {
  const client = getClient();
  const property = formatPropertyId(propertyId);

  const [response] = await client.runRealtimeReport({
    property,
    dimensions: [{ name: 'unifiedScreenName' }],
    metrics: [{ name: 'activeUsers' }],
  });

  if (save) {
    saveResult(response, 'realtime', 'active_users');
  }

  return response as RealtimeResponse;
}

/**
 * Get current event data
 */
export async function getRealtimeEvents(propertyId: string, save = true): Promise<RealtimeResponse> {
  const client = getClient();
  const property = formatPropertyId(propertyId);

  const [response] = await client.runRealtimeReport({
    property,
    dimensions: [{ name: 'eventName' }],
    metrics: [{ name: 'eventCount' }],
  });

  if (save) {
    saveResult(response, 'realtime', 'events');
  }

  return response as RealtimeResponse;
}

/**
 * Get currently viewed pages
 */
export async function getRealtimePages(propertyId: string, save = true): Promise<RealtimeResponse> {
  const client = getClient();
  const property = formatPropertyId(propertyId);

  const [response] = await client.runRealtimeReport({
    property,
    dimensions: [{ name: 'unifiedScreenName' }],
    metrics: [{ name: 'screenPageViews' }],
  });

  if (save) {
    saveResult(response, 'realtime', 'pages');
  }

  return response as RealtimeResponse;
}

/**
 * Get realtime traffic sources
 */
export async function getRealtimeSources(propertyId: string, save = true): Promise<RealtimeResponse> {
  const client = getClient();
  const property = formatPropertyId(propertyId);

  const [response] = await client.runRealtimeReport({
    property,
    dimensions: [{ name: 'firstUserSource' }, { name: 'firstUserMedium' }],
    metrics: [{ name: 'activeUsers' }],
  });

  if (save) {
    saveResult(response, 'realtime', 'sources');
  }

  return response as RealtimeResponse;
}


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)