# Google Analytics Client

Run reports, get real-time data, and interact with Google Analytics 4 properties.

## Methods

| Method                                   | Description                                                  |
| ---------------------------------------- | ------------------------------------------------------------ |
| `apiRequest(options, schema, metadata?)` | Make any Google Analytics API request with schema validation |

## Usage

### Run a Report

```typescript
import { api, z, googleAnalytics } from "@superblocksteam/sdk-api";

// Integration ID from the integrations panel
const PROD_GA4 = "a1b2c3d4-5678-90ab-cdef-ga4000000001";

const ReportRowSchema = z.object({
  dimensionValues: z.array(
    z.object({
      value: z.string(),
    }),
  ),
  metricValues: z.array(
    z.object({
      value: z.string(),
    }),
  ),
});

const ReportResponseSchema = z.object({
  dimensionHeaders: z.array(z.object({ name: z.string() })),
  metricHeaders: z.array(z.object({ name: z.string(), type: z.string() })),
  rows: z.array(ReportRowSchema).optional(),
  rowCount: z.number().optional(),
  metadata: z
    .object({
      currencyCode: z.string().optional(),
      timeZone: z.string().optional(),
    })
    .optional(),
});

export default api({
  integrations: {
    ga: googleAnalytics(PROD_GA4),
  },
  name: "GoogleAnalyticsExample",
  input: z.object({
    propertyId: z.string(),
    startDate: z.string(),
    endDate: z.string(),
  }),
  output: z.object({
    pageViews: z.array(
      z.object({
        pagePath: z.string(),
        views: z.number(),
      }),
    ),
  }),
  async run(ctx, { propertyId, startDate, endDate }) {
    const result = await ctx.integrations.ga.apiRequest(
      {
        method: "POST",
        path: `/v1beta/properties/${propertyId}:runReport`,
        body: {
          dateRanges: [
            {
              startDate: startDate,
              endDate: endDate,
            },
          ],
          dimensions: [{ name: "pagePath" }],
          metrics: [{ name: "screenPageViews" }],
          limit: 100,
          orderBys: [
            {
              metric: { metricName: "screenPageViews" },
              desc: true,
            },
          ],
        },
      },
      { response: ReportResponseSchema },
    );

    return {
      pageViews: (result.rows ?? []).map((row) => ({
        pagePath: row.dimensionValues[0].value,
        views: parseInt(row.metricValues[0].value, 10),
      })),
    };
  },
});
```

### Get Real-Time Report

```typescript
const RealTimeResponseSchema = z.object({
  dimensionHeaders: z.array(z.object({ name: z.string() })).optional(),
  metricHeaders: z.array(z.object({ name: z.string() })).optional(),
  rows: z.array(ReportRowSchema).optional(),
  rowCount: z.number().optional(),
});

const result = await ctx.integrations.ga.apiRequest(
  {
    method: "POST",
    path: `/v1beta/properties/${propertyId}:runRealtimeReport`,
    body: {
      dimensions: [{ name: "country" }],
      metrics: [{ name: "activeUsers" }],
      limit: 10,
    },
  },
  { response: RealTimeResponseSchema },
);

result.rows?.forEach((row) => {
  console.log(
    `${row.dimensionValues[0].value}: ${row.metricValues[0].value} active users`,
  );
});
```

### Run Multiple Reports (Batch)

```typescript
const BatchReportResponseSchema = z.object({
  reports: z.array(ReportResponseSchema),
});

const result = await ctx.integrations.ga.apiRequest(
  {
    method: "POST",
    path: `/v1beta/properties/${propertyId}:batchRunReports`,
    body: {
      requests: [
        {
          dateRanges: [{ startDate: "30daysAgo", endDate: "today" }],
          dimensions: [{ name: "deviceCategory" }],
          metrics: [{ name: "sessions" }],
        },
        {
          dateRanges: [{ startDate: "30daysAgo", endDate: "today" }],
          dimensions: [{ name: "browser" }],
          metrics: [{ name: "sessions" }],
        },
      ],
    },
  },
  { response: BatchReportResponseSchema },
);

// Access individual reports
const deviceReport = result.reports[0];
const browserReport = result.reports[1];
```

### Get User Metrics with Segments

```typescript
const result = await ctx.integrations.ga.apiRequest(
  {
    method: "POST",
    path: `/v1beta/properties/${propertyId}:runReport`,
    body: {
      dateRanges: [{ startDate: "7daysAgo", endDate: "today" }],
      dimensions: [{ name: "date" }],
      metrics: [
        { name: "totalUsers" },
        { name: "newUsers" },
        { name: "activeUsers" },
        { name: "sessions" },
        { name: "bounceRate" },
        { name: "averageSessionDuration" },
      ],
      dimensionFilter: {
        filter: {
          fieldName: "country",
          stringFilter: {
            matchType: "EXACT",
            value: "United States",
          },
        },
      },
      orderBys: [{ dimension: { dimensionName: "date" } }],
    },
  },
  { response: ReportResponseSchema },
);
```

### List Account Summaries

```typescript
const AccountSummarySchema = z.object({
  name: z.string(),
  account: z.string(),
  displayName: z.string(),
  propertySummaries: z
    .array(
      z.object({
        property: z.string(),
        displayName: z.string(),
        propertyType: z.string(),
      }),
    )
    .optional(),
});

const ListAccountSummariesSchema = z.object({
  accountSummaries: z.array(AccountSummarySchema).optional(),
  nextPageToken: z.string().optional(),
});

const result = await ctx.integrations.ga.apiRequest(
  {
    method: "GET",
    path: "/v1beta/accountSummaries",
    params: {
      pageSize: 50,
    },
  },
  { response: ListAccountSummariesSchema },
);

result.accountSummaries?.forEach((account) => {
  console.log(`Account: ${account.displayName}`);
  account.propertySummaries?.forEach((property) => {
    console.log(`  Property: ${property.displayName} (${property.property})`);
  });
});
```

## Trace Metadata

All methods accept an optional `metadata` parameter as the last argument for diagnostics labeling. See the [root SDK README](../../../README.md#trace-metadata) for details.

## Common Pitfalls

### No Specialized Methods

```typescript
// WRONG - These methods do not exist
await ga.runReport({ ... });
await ga.getRealtimeData();

// CORRECT - Use apiRequest
await ctx.integrations.ga.apiRequest(
  { method: "POST", path: `/v1beta/properties/${propertyId}:runReport`, body: { ... } },
  { response: ReportResponseSchema }
);
```

### Property ID Format

Property IDs should be numeric only, not the full resource name:

```typescript
// WRONG - Including "properties/" prefix
const path = `/v1beta/properties/properties/123456789:runReport`;

// CORRECT - Numeric ID only
const propertyId = "123456789";
const path = `/v1beta/properties/${propertyId}:runReport`;
```

### Metric Values are Strings

All metric values are returned as strings:

```typescript
// WRONG - Expecting numbers
const schema = z.object({
  metricValues: z.array(z.object({ value: z.number() })),
});

// CORRECT - Parse as strings, convert as needed
const schema = z.object({
  metricValues: z.array(z.object({ value: z.string() })),
});

// Convert when using
const views = parseInt(row.metricValues[0].value, 10);
const rate = parseFloat(row.metricValues[1].value);
```

### Date Formats

Use specific formats for dates:

```typescript
// Relative dates (recommended for dynamic reports)
const startDate = "30daysAgo";
const startDate = "7daysAgo";
const startDate = "yesterday";
const startDate = "today";

// Absolute dates (YYYY-MM-DD format)
const startDate = "2024-01-01";
const endDate = "2024-01-31";

// WRONG - Other formats
const startDate = "01/01/2024"; // No slash format
const startDate = "Jan 1, 2024"; // No text format
```

### Dimension and Metric Names

Use API names, not display names:

```typescript
// WRONG - Display names
const dimensions = [{ name: "Page Path" }];
const metrics = [{ name: "Page Views" }];

// CORRECT - API names (camelCase)
const dimensions = [{ name: "pagePath" }];
const metrics = [{ name: "screenPageViews" }];

// Common mappings:
// "Page Views" -> "screenPageViews"
// "Sessions" -> "sessions"
// "Users" -> "totalUsers"
// "Bounce Rate" -> "bounceRate"
// "Country" -> "country"
// "Device Category" -> "deviceCategory"
```

### Empty Results Handling

Reports may return no rows:

```typescript
// WRONG - Assuming rows exist
const firstRow = result.rows[0]; // TypeError if no rows

// CORRECT - Handle empty results
const rows = result.rows ?? [];
if (rows.length === 0) {
  return { data: [] };
}

const firstRow = rows[0];
```

### API Version

GA4 uses the Analytics Data API (v1beta):

```typescript
// WRONG - Old Universal Analytics API
const path = "/analytics/v3/data/ga";

// CORRECT - GA4 Data API
const path = `/v1beta/properties/${propertyId}:runReport`;
```

### Quotas and Sampling

Large reports may be sampled:

```typescript
const ReportResponseSchema = z.object({
  rows: z.array(ReportRowSchema).optional(),
  metadata: z
    .object({
      samplingMetadatas: z
        .array(
          z.object({
            samplesReadCount: z.string(),
            samplingSpaceSize: z.string(),
          }),
        )
        .optional(),
    })
    .optional(),
});

// Check if data was sampled
if (result.metadata?.samplingMetadatas) {
  console.warn("Report data was sampled - results are estimates");
}
```

## Error Handling

```typescript
import { RestApiValidationError } from "@superblocksteam/sdk-api";

try {
  const result = await ctx.integrations.ga.apiRequest(
    { method: "POST", path: `/v1beta/properties/${propertyId}:runReport`, body: { ... } },
    { response: ReportResponseSchema }
  );
} catch (error) {
  if (error instanceof RestApiValidationError) {
    console.error("Validation failed:", error.details.zodError);
  }
}
```

## API Reference

- [Google Analytics Data API](https://developers.google.com/analytics/devguides/reporting/data/v1)
- [Dimensions & Metrics Explorer](https://developers.google.com/analytics/devguides/reporting/data/v1/api-schema)
- [Real-time Reports](https://developers.google.com/analytics/devguides/reporting/data/v1/realtime-basics)
- [API Quotas](https://developers.google.com/analytics/devguides/reporting/data/v1/quotas)
