From 68f42fbaa5b623aaf9cde86fafbd145c4315622d Mon Sep 17 00:00:00 2001 From: yuriassuncx Date: Wed, 24 Jun 2026 17:49:18 -0300 Subject: [PATCH 1/5] fix(google-analytics): fix input validation and align output schemas with other Google MCPs - Add `fromJson` preprocessor (z.preprocess) to handle MCP clients that serialize arrays and objects as JSON strings instead of native types. Fixes validation errors for dateRanges, dimensions, metrics, dimensionFilter, metricFilter, orderBys, limit, offset, and funnelSteps. - Remove the `response` wrapper from all output schemas so tools return data directly (e.g. `{ rows: [...] }` instead of `{ response: { rows: [...] } }`), consistent with google-search-console and google-tag-manager patterns. - Flatten `getCustomDimensionsAndMetrics` output from `{ dimensions: { customDimensions: [...] }, metrics: { customMetrics: [...] } }` to `{ customDimensions: [...], customMetrics: [...] }`. - Fix code formatting: remove unnecessary blank lines between chained Zod method calls across all tool files. Co-Authored-By: Claude Sonnet 4.6 --- google-analytics/server/lib/schemas.ts | 87 +------ google-analytics/server/tools/accounts.ts | 7 +- google-analytics/server/tools/ads.ts | 9 +- google-analytics/server/tools/annotations.ts | 9 +- google-analytics/server/tools/properties.ts | 22 +- google-analytics/server/tools/reports.ts | 236 +++++-------------- 6 files changed, 87 insertions(+), 283 deletions(-) diff --git a/google-analytics/server/lib/schemas.ts b/google-analytics/server/lib/schemas.ts index cb336b72..746f4576 100644 --- a/google-analytics/server/lib/schemas.ts +++ b/google-analytics/server/lib/schemas.ts @@ -10,10 +10,8 @@ import { z } from "zod"; // ── Shared primitives ──────────────────────────────────────────────────────── -/** ISO-8601 timestamp string (e.g. "2024-01-15T10:30:00Z"). */ const Timestamp = z.string(); -/** Represents a date used in annotation. */ const DateSchema = z.object({ year: z.number().int().optional(), month: z.number().int().optional(), @@ -23,36 +21,27 @@ const DateSchema = z.object({ // ── Admin API — Account Summaries ──────────────────────────────────────────── export const PropertySummarySchema = z.object({ - /** Resource name, e.g. "properties/123456". */ property: z.string(), displayName: z.string(), - /** e.g. "PROPERTY_TYPE_ORDINARY" | "PROPERTY_TYPE_ROLLUP" | "PROPERTY_TYPE_SUBPROPERTY" */ propertyType: z.string().optional(), - /** Parent resource name, e.g. "accounts/123456". */ parent: z.string().optional(), }); export const AccountSummarySchema = z.object({ - /** Resource name, e.g. "accountSummaries/123456". */ name: z.string(), - /** Account resource name, e.g. "accounts/123456". */ account: z.string().optional(), displayName: z.string().optional(), propertySummaries: z.array(PropertySummarySchema).optional(), }); -export const AccountSummariesResponseSchema = z.object({ - response: z.object({ - accountSummaries: z.array(AccountSummarySchema).optional(), - }), +export const AccountSummariesOutputSchema = z.object({ + accountSummaries: z.array(AccountSummarySchema).optional(), }); // ── Admin API — Property ───────────────────────────────────────────────────── export const PropertySchema = z.object({ - /** Resource name, e.g. "properties/123456". */ name: z.string().optional(), - /** Parent resource, e.g. "accounts/123456" or "properties/123" for sub-properties. */ parent: z.string().optional(), createTime: Timestamp.optional(), updateTime: Timestamp.optional(), @@ -60,68 +49,43 @@ export const PropertySchema = z.object({ industryCategory: z.string().optional(), timeZone: z.string().optional(), currencyCode: z.string().optional(), - /** e.g. "GOOGLE_ANALYTICS_STANDARD" | "GOOGLE_ANALYTICS_360" */ serviceLevel: z.string().optional(), - /** e.g. "PROPERTY_TYPE_ORDINARY" | "PROPERTY_TYPE_ROLLUP" | "PROPERTY_TYPE_SUBPROPERTY" */ propertyType: z.string().optional(), account: z.string().optional(), deleteTime: Timestamp.optional(), expireTime: Timestamp.optional(), }); -export const PropertyResponseSchema = z.object({ - response: PropertySchema, -}); - // ── Admin API — Custom Dimensions & Metrics ────────────────────────────────── export const CustomDimensionSchema = z.object({ - /** Resource name, e.g. "properties/123/customDimensions/456". */ name: z.string().optional(), - /** The parameter name used in events / user properties. */ parameterName: z.string(), displayName: z.string().optional(), description: z.string().optional(), - /** "EVENT" | "USER" | "ITEM" */ scope: z.string().optional(), disallowAdsPersonalization: z.boolean().optional(), }); export const CustomMetricSchema = z.object({ - /** Resource name. */ name: z.string().optional(), parameterName: z.string(), displayName: z.string().optional(), description: z.string().optional(), - /** e.g. "STANDARD" | "CURRENCY" | "FEET" | "METERS" | "KILOMETERS" | "MILES" | "MILLISECONDS" | "SECONDS" | "MINUTES" | "HOURS" */ measurementUnit: z.string().optional(), - /** "EVENT" | "ITEM" */ scope: z.string().optional(), - /** e.g. ["COST_DATA", "REVENUE_DATA"] */ restrictedMetricType: z.array(z.string()).optional(), }); -export const CustomDimensionsListSchema = z.object({ +export const CustomDimensionsAndMetricsOutputSchema = z.object({ customDimensions: z.array(CustomDimensionSchema).optional(), -}); - -export const CustomMetricsListSchema = z.object({ customMetrics: z.array(CustomMetricSchema).optional(), }); -export const CustomDimensionsAndMetricsResponseSchema = z.object({ - /** Result of listCustomDimensions — `{ customDimensions: [...] }` */ - dimensions: CustomDimensionsListSchema, - /** Result of listCustomMetrics — `{ customMetrics: [...] }` */ - metrics: CustomMetricsListSchema, -}); - // ── Admin API — Google Ads Links ───────────────────────────────────────────── export const GoogleAdsLinkSchema = z.object({ - /** Resource name. */ name: z.string().optional(), - /** Google Ads Customer ID (without hyphens). */ customerId: z.string().optional(), canManageClients: z.boolean().optional(), adsPersonalizationEnabled: z.boolean().optional(), @@ -130,29 +94,23 @@ export const GoogleAdsLinkSchema = z.object({ updateTime: Timestamp.optional(), }); -export const GoogleAdsLinksResponseSchema = z.object({ - response: z.object({ - googleAdsLinks: z.array(GoogleAdsLinkSchema).optional(), - }), +export const GoogleAdsLinksOutputSchema = z.object({ + googleAdsLinks: z.array(GoogleAdsLinkSchema).optional(), }); // ── Admin API — Property Annotations ───────────────────────────────────────── export const ReportingDataAnnotationSchema = z.object({ - /** Resource name. */ name: z.string().optional(), annotationDate: DateSchema.optional(), title: z.string().optional(), description: z.string().optional(), systemGenerated: z.boolean().optional(), - /** "RED" | "ORANGE" | "YELLOW" | "GREEN" | "BLUE" | "PURPLE" | "PINK" */ color: z.string().optional(), }); -export const PropertyAnnotationsResponseSchema = z.object({ - response: z.object({ - reportingDataAnnotations: z.array(ReportingDataAnnotationSchema).optional(), - }), +export const PropertyAnnotationsOutputSchema = z.object({ + reportingDataAnnotations: z.array(ReportingDataAnnotationSchema).optional(), }); // ── Data API — shared building blocks ──────────────────────────────────────── @@ -163,7 +121,6 @@ export const DimensionHeaderSchema = z.object({ export const MetricHeaderSchema = z.object({ name: z.string(), - /** e.g. "TYPE_INTEGER" | "TYPE_FLOAT" | "TYPE_SECONDS" | "TYPE_MILLISECONDS" | "TYPE_MINUTES" | "TYPE_HOURS" | "TYPE_STANDARD" | "TYPE_CURRENCY" | "TYPE_FEET" | "TYPE_MILES" | "TYPE_METERS" | "TYPE_KILOMETERS" */ type: z.string().optional(), }); @@ -188,7 +145,6 @@ export const ResponseMetaDataSchema = z.object({ samplingMetadatas: z.array(z.unknown()).optional(), }); -/** Quota state returned when `returnPropertyQuota: true`. */ export const PropertyQuotaSchema = z .object({ tokensPerDay: z @@ -214,7 +170,7 @@ export const PropertyQuotaSchema = z // ── Data API — runReport ────────────────────────────────────────────────────── -export const RunReportResponseSchema = z.object({ +export const RunReportOutputSchema = z.object({ dimensionHeaders: z.array(DimensionHeaderSchema).optional(), metricHeaders: z.array(MetricHeaderSchema).optional(), rows: z.array(RowSchema).optional(), @@ -224,17 +180,12 @@ export const RunReportResponseSchema = z.object({ rowCount: z.number().optional(), metadata: ResponseMetaDataSchema.optional(), propertyQuota: PropertyQuotaSchema.optional(), - /** Always "analyticsData#runReport". */ kind: z.string().optional(), }); -export const RunReportOutputSchema = z.object({ - response: RunReportResponseSchema, -}); - // ── Data API — runRealtimeReport ───────────────────────────────────────────── -export const RunRealtimeReportResponseSchema = z.object({ +export const RunRealtimeReportOutputSchema = z.object({ dimensionHeaders: z.array(DimensionHeaderSchema).optional(), metricHeaders: z.array(MetricHeaderSchema).optional(), rows: z.array(RowSchema).optional(), @@ -243,26 +194,17 @@ export const RunRealtimeReportResponseSchema = z.object({ minimums: z.array(RowSchema).optional(), rowCount: z.number().optional(), propertyQuota: PropertyQuotaSchema.optional(), - /** Always "analyticsData#runRealtimeReport". */ kind: z.string().optional(), }); -export const RunRealtimeReportOutputSchema = z.object({ - response: RunRealtimeReportResponseSchema, -}); - // ── Data API — runFunnelReport ──────────────────────────────────────────────── -export const FunnelResponseMetaDataSchema = z.object({ +const FunnelResponseMetaDataSchema = z.object({ samplingMetadatas: z.array(z.unknown()).optional(), schemaRestrictionResponse: z.unknown().optional(), }); -/** - * Funnel response rows share the same Row structure (dimensionValues + - * metricValues), grouped in funnelTable and funnelVisualization sub-objects. - */ -export const FunnelSubReportSchema = z.object({ +const FunnelSubReportSchema = z.object({ dimensionHeaders: z.array(DimensionHeaderSchema).optional(), metricHeaders: z.array(MetricHeaderSchema).optional(), rows: z.array(RowSchema).optional(), @@ -273,14 +215,9 @@ export const FunnelSubReportSchema = z.object({ metadata: FunnelResponseMetaDataSchema.optional(), }); -export const RunFunnelReportResponseSchema = z.object({ +export const RunFunnelReportOutputSchema = z.object({ funnelTable: FunnelSubReportSchema.optional(), funnelVisualization: FunnelSubReportSchema.optional(), propertyQuota: PropertyQuotaSchema.optional(), - /** Always "analyticsData#runFunnelReport". */ kind: z.string().optional(), }); - -export const RunFunnelReportOutputSchema = z.object({ - response: RunFunnelReportResponseSchema, -}); diff --git a/google-analytics/server/tools/accounts.ts b/google-analytics/server/tools/accounts.ts index 7b0650b3..c283fed1 100644 --- a/google-analytics/server/tools/accounts.ts +++ b/google-analytics/server/tools/accounts.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { createPrivateTool } from "@decocms/runtime/tools"; import type { Env } from "../../shared/deco.gen.ts"; import { GaClient } from "../lib/ga-client.ts"; -import { AccountSummariesResponseSchema } from "../lib/schemas.ts"; +import { AccountSummariesOutputSchema } from "../lib/schemas.ts"; export const getAccountSummariesTool = (env: Env) => createPrivateTool({ @@ -10,13 +10,12 @@ export const getAccountSummariesTool = (env: Env) => description: "Retrieves information about the user's Google Analytics accounts and properties.", inputSchema: z.object({}), - outputSchema: AccountSummariesResponseSchema, + outputSchema: AccountSummariesOutputSchema, execute: async () => { const client = GaClient.fromEnv(env); - try { const result = await client.listAccountSummaries(); - return AccountSummariesResponseSchema.parse({ response: result }); + return AccountSummariesOutputSchema.parse(result); } catch (error) { throw new Error( `Failed to retrieve account summaries: ${error instanceof Error ? error.message : String(error)}`, diff --git a/google-analytics/server/tools/ads.ts b/google-analytics/server/tools/ads.ts index 342bfbce..959ae27b 100644 --- a/google-analytics/server/tools/ads.ts +++ b/google-analytics/server/tools/ads.ts @@ -3,14 +3,11 @@ import { createPrivateTool } from "@decocms/runtime/tools"; import type { Env } from "../../shared/deco.gen.ts"; import { GaClient } from "../lib/ga-client.ts"; import { resolveProperty } from "../lib/env.ts"; -import { GoogleAdsLinksResponseSchema } from "../lib/schemas.ts"; +import { GoogleAdsLinksOutputSchema } from "../lib/schemas.ts"; const propertySchema = z - .string() - .optional() - .describe( "GA4 Property identifier — 'properties/1234567' or just '1234567'. Falls back to the default propertyId configured for this integration when omitted.", ); @@ -21,14 +18,14 @@ export const listGoogleAdsLinksTool = (env: Env) => description: "Returns a list of links to Google Ads accounts for a GA4 property.", inputSchema: z.object({ property: propertySchema }), - outputSchema: GoogleAdsLinksResponseSchema, + outputSchema: GoogleAdsLinksOutputSchema, execute: async ({ context: args }) => { const client = GaClient.fromEnv(env); try { const result = await client.listGoogleAdsLinks( resolveProperty(env, args.property), ); - return GoogleAdsLinksResponseSchema.parse({ response: result }); + return GoogleAdsLinksOutputSchema.parse(result); } catch (error) { throw new Error( `Failed to retrieve Google Ads links: ${error instanceof Error ? error.message : String(error)}`, diff --git a/google-analytics/server/tools/annotations.ts b/google-analytics/server/tools/annotations.ts index b170c95f..ca720d85 100644 --- a/google-analytics/server/tools/annotations.ts +++ b/google-analytics/server/tools/annotations.ts @@ -3,14 +3,11 @@ import { createPrivateTool } from "@decocms/runtime/tools"; import type { Env } from "../../shared/deco.gen.ts"; import { GaClient } from "../lib/ga-client.ts"; import { resolveProperty } from "../lib/env.ts"; -import { PropertyAnnotationsResponseSchema } from "../lib/schemas.ts"; +import { PropertyAnnotationsOutputSchema } from "../lib/schemas.ts"; const propertySchema = z - .string() - .optional() - .describe( "GA4 Property identifier — 'properties/1234567' or just '1234567'. Falls back to the default propertyId configured for this integration when omitted.", ); @@ -21,14 +18,14 @@ export const listPropertyAnnotationsTool = (env: Env) => description: "Returns timestamped annotations for a GA4 property — useful for correlating traffic changes with events like campaign launches, site releases, or data collection changes.", inputSchema: z.object({ property: propertySchema }), - outputSchema: PropertyAnnotationsResponseSchema, + outputSchema: PropertyAnnotationsOutputSchema, execute: async ({ context: args }) => { const client = GaClient.fromEnv(env); try { const result = await client.listPropertyAnnotations( resolveProperty(env, args.property), ); - return PropertyAnnotationsResponseSchema.parse({ response: result }); + return PropertyAnnotationsOutputSchema.parse(result); } catch (error) { throw new Error( `Failed to retrieve property annotations: ${error instanceof Error ? error.message : String(error)}`, diff --git a/google-analytics/server/tools/properties.ts b/google-analytics/server/tools/properties.ts index 6258684d..6a4cfefb 100644 --- a/google-analytics/server/tools/properties.ts +++ b/google-analytics/server/tools/properties.ts @@ -4,16 +4,13 @@ import type { Env } from "../../shared/deco.gen.ts"; import { GaClient } from "../lib/ga-client.ts"; import { resolveProperty } from "../lib/env.ts"; import { - PropertyResponseSchema, - CustomDimensionsAndMetricsResponseSchema, + PropertySchema, + CustomDimensionsAndMetricsOutputSchema, } from "../lib/schemas.ts"; const propertySchema = z - .string() - .optional() - .describe( "GA4 Property identifier — 'properties/1234567' or just '1234567'. Falls back to the default propertyId configured for this integration when omitted.", ); @@ -24,14 +21,14 @@ export const getPropertyDetailsTool = (env: Env) => description: "Returns metadata and configuration details about a GA4 property.", inputSchema: z.object({ property: propertySchema }), - outputSchema: PropertyResponseSchema, + outputSchema: PropertySchema, execute: async ({ context: args }) => { const client = GaClient.fromEnv(env); try { const result = await client.getProperty( resolveProperty(env, args.property), ); - return PropertyResponseSchema.parse({ response: result }); + return PropertySchema.parse(result); } catch (error) { throw new Error( `Failed to retrieve property details: ${error instanceof Error ? error.message : String(error)}`, @@ -46,20 +43,19 @@ export const getCustomDimensionsAndMetricsTool = (env: Env) => description: "Retrieves the custom dimensions and custom metrics configured for a GA4 property.", inputSchema: z.object({ property: propertySchema }), - outputSchema: CustomDimensionsAndMetricsResponseSchema, + outputSchema: CustomDimensionsAndMetricsOutputSchema, execute: async ({ context: args }) => { const client = GaClient.fromEnv(env); try { const property = resolveProperty(env, args.property); // Both calls are independent — run in parallel to halve latency. - const [dimensions, metrics] = await Promise.all([ + const [dimensionsResult, metricsResult] = await Promise.all([ client.listCustomDimensions(property), client.listCustomMetrics(property), ]); - - return CustomDimensionsAndMetricsResponseSchema.parse({ - dimensions, - metrics, + return CustomDimensionsAndMetricsOutputSchema.parse({ + ...(dimensionsResult as object), + ...(metricsResult as object), }); } catch (error) { throw new Error( diff --git a/google-analytics/server/tools/reports.ts b/google-analytics/server/tools/reports.ts index 6b5c43c3..7b78f13f 100644 --- a/google-analytics/server/tools/reports.ts +++ b/google-analytics/server/tools/reports.ts @@ -9,18 +9,26 @@ import { RunRealtimeReportOutputSchema, } from "../lib/schemas.ts"; +// Some MCP clients serialize arrays/objects as JSON strings instead of native +// types. This preprocessor parses the string before Zod validates it. +const fromJson = (schema: T) => + z.preprocess((val) => { + if (typeof val !== "string") return val; + try { + return JSON.parse(val); + } catch { + return val; + } + }, schema); + const DateRangeSchema = z.object({ startDate: z - .string() - .describe( "Inclusive start date in YYYY-MM-DD format, or relative values: 'today', 'yesterday', 'NdaysAgo'.", ), endDate: z - .string() - .describe( "Inclusive end date in YYYY-MM-DD format, or relative values: 'today', 'yesterday'.", ), @@ -31,17 +39,13 @@ const MetricSchema = z.object({ name: z.string() }); // FilterExpression and OrderBy are passed as raw JSON objects matching the GA4 REST API schema. const FilterExpressionSchema = z - .record(z.string(), z.unknown()) - .describe( "GA4 FilterExpression object. See https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/FilterExpression", ); const OrderBySchema = z - .record(z.string(), z.unknown()) - .describe( "GA4 OrderBy object. See https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/OrderBy", ); @@ -53,91 +57,50 @@ export const runReportTool = (env: Env) => "Runs a Google Analytics 4 report using the Data API. Returns dimensions, metrics, and row data for the specified property and date range.", inputSchema: z.object({ property: z - .string() - .optional() - .describe( "GA4 Property identifier — 'properties/1234567' or just '1234567'. Falls back to the default propertyId configured for this integration when omitted.", ), - dateRanges: z - - .array(DateRangeSchema) - - .min(1) - - .describe("One or more date ranges to include in the report."), - dimensions: z - - .array(DimensionSchema) - + dateRanges: fromJson(z.array(DateRangeSchema).min(1)).describe( + "One or more date ranges to include in the report.", + ), + dimensions: fromJson(z.array(DimensionSchema)) .optional() - .describe( "Dimensions to group results by, e.g. [{ name: 'sessionSource' }].", ), - metrics: z - - .array(MetricSchema) - - .min(1) - - .describe("Metrics to aggregate, e.g. [{ name: 'activeUsers' }]."), - dimensionFilter: FilterExpressionSchema.optional().describe( - "Optional filter to restrict dimension values.", - ), - metricFilter: FilterExpressionSchema.optional().describe( - "Optional filter to restrict metric values.", + metrics: fromJson(z.array(MetricSchema).min(1)).describe( + "Metrics to aggregate, e.g. [{ name: 'activeUsers' }].", ), - orderBys: z - - .array(OrderBySchema) - + dimensionFilter: fromJson(FilterExpressionSchema) + .optional() + .describe("Optional filter to restrict dimension values."), + metricFilter: fromJson(FilterExpressionSchema) + .optional() + .describe("Optional filter to restrict metric values."), + orderBys: fromJson(z.array(OrderBySchema)) .optional() - .describe("Optional ordering for returned rows."), - limit: z - - .number() - - .int() - - .positive() - + limit: fromJson(z.number().int().positive()) .optional() - .describe( "Maximum number of rows to return. Defaults to 10,000; max 250,000.", ), - offset: z - - .number() - - .int() - - .nonnegative() - + offset: fromJson(z.number().int().nonnegative()) .optional() - .describe( "Row offset for pagination (0-based). Use with limit to page through results.", ), currencyCode: z - .string() - .optional() - .describe( "Optional ISO 4217 currency code for revenue metrics, e.g. 'USD'.", ), returnPropertyQuota: z - .boolean() - .optional() - .describe( "If true, includes the current GA4 property quota state in the response.", ), @@ -160,15 +123,13 @@ export const runReportTool = (env: Env) => if (args.offset !== undefined) body.offset = args.offset; if (args.currencyCode !== undefined) body.currencyCode = args.currencyCode; - if (args.returnPropertyQuota !== undefined) { + if (args.returnPropertyQuota !== undefined) body.returnPropertyQuota = args.returnPropertyQuota; - } const response = await client.runReport( resolveProperty(env, args.property), body, ); - - return RunReportOutputSchema.parse({ response }); + return RunReportOutputSchema.parse(response); } catch (error) { throw new Error( `Failed to run report: ${error instanceof Error ? error.message : String(error)}`, @@ -179,32 +140,20 @@ export const runReportTool = (env: Env) => const FunnelStepSchema = z.object({ name: z - .string() - .describe("Display name for this funnel step (shown in reports)."), - filterExpression: z - - .record(z.string(), z.unknown()) - - .describe( - "Required. Condition that qualifies users for this step. See https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/FunnelStep#FunnelFilterExpression", - ), + filterExpression: fromJson(z.record(z.string(), z.unknown())).describe( + "Required. Condition that qualifies users for this step. See https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/FunnelStep#FunnelFilterExpression", + ), isDirectlyFollowedBy: z - .boolean() - .optional() - .describe( "If true, this step must immediately follow the previous step (no intervening events). Defaults to false.", ), withinDurationFromPriorStep: z - .string() - .optional() - .describe( "Time window within which this step must occur after the prior step, e.g. '3600s' for 1 hour.", ), @@ -217,76 +166,36 @@ export const runFunnelReportTool = (env: Env) => "Runs a Google Analytics 4 funnel report. Analyzes how users progress through a sequence of steps (e.g. landing page → product page → checkout → purchase). Returns per-step completion counts and drop-off rates.", inputSchema: z.object({ property: z - .string() - .optional() - .describe( "GA4 Property identifier — 'properties/1234567' or just '1234567'. Falls back to the default propertyId configured for this integration when omitted.", ), - funnelSteps: z - - .array(FunnelStepSchema) - - .min(2) - - .describe( - "Ordered list of funnel steps. Each step is a GA4 FunnelStep object with a name and filterExpression.", - ), - dateRanges: z - - .array(DateRangeSchema) - - .min(1) - - .describe("One or more date ranges to include in the report."), - funnelBreakdown: z - - .record(z.string(), z.unknown()) - + funnelSteps: fromJson(z.array(FunnelStepSchema).min(2)).describe( + "Ordered list of funnel steps. Each step is a GA4 FunnelStep object with a name and filterExpression.", + ), + dateRanges: fromJson(z.array(DateRangeSchema).min(1)).describe( + "One or more date ranges to include in the report.", + ), + funnelBreakdown: fromJson(z.record(z.string(), z.unknown())) .optional() - .describe( "Optional FunnelBreakdown — adds a sub-dimension to the funnel table rows.", ), - funnelNextAction: z - - .record(z.string(), z.unknown()) - + funnelNextAction: fromJson(z.record(z.string(), z.unknown())) .optional() - .describe( "Optional FunnelNextAction — adds a next-action dimension showing what users do after abandoning a step.", ), - limit: z - - .number() - - .int() - - .positive() - + limit: fromJson(z.number().int().positive()) .optional() - .describe("Maximum number of rows to return."), - offset: z - - .number() - - .int() - - .nonnegative() - + offset: fromJson(z.number().int().nonnegative()) .optional() - .describe("Row offset for pagination."), returnPropertyQuota: z - .boolean() - .optional() - .describe("If true, includes current GA4 property quota in response."), }), outputSchema: RunFunnelReportOutputSchema, @@ -309,8 +218,7 @@ export const runFunnelReportTool = (env: Env) => resolveProperty(env, args.property), body, ); - - return RunFunnelReportOutputSchema.parse({ response }); + return RunFunnelReportOutputSchema.parse(response); } catch (error) { throw new Error( `Failed to run funnel report: ${error instanceof Error ? error.message : String(error)}`, @@ -326,63 +234,35 @@ export const runRealtimeReportTool = (env: Env) => "Runs a Google Analytics 4 realtime report. Returns live data from the last 30 minutes.", inputSchema: z.object({ property: z - .string() - .optional() - .describe( "GA4 Property identifier — 'properties/1234567' or just '1234567'. Falls back to the default propertyId configured for this integration when omitted.", ), - dimensions: z - - .array(DimensionSchema) - + dimensions: fromJson(z.array(DimensionSchema)) .optional() - .describe("Dimensions to group results by."), - metrics: z.array(MetricSchema).min(1).describe("Metrics to aggregate."), - dimensionFilter: FilterExpressionSchema.optional().describe( - "Optional filter to restrict dimension values.", + metrics: fromJson(z.array(MetricSchema).min(1)).describe( + "Metrics to aggregate.", ), - metricFilter: FilterExpressionSchema.optional().describe( - "Optional filter to restrict metric values.", - ), - orderBys: z - - .array(OrderBySchema) - + dimensionFilter: fromJson(FilterExpressionSchema) + .optional() + .describe("Optional filter to restrict dimension values."), + metricFilter: fromJson(FilterExpressionSchema) + .optional() + .describe("Optional filter to restrict metric values."), + orderBys: fromJson(z.array(OrderBySchema)) .optional() - .describe("Optional ordering for returned rows."), - limit: z - - .number() - - .int() - - .positive() - + limit: fromJson(z.number().int().positive()) .optional() - .describe("Maximum number of rows to return."), - offset: z - - .number() - - .int() - - .nonnegative() - + offset: fromJson(z.number().int().nonnegative()) .optional() - .describe("Row offset for pagination."), returnPropertyQuota: z - .boolean() - .optional() - .describe("If true, includes quota state in the response."), }), outputSchema: RunRealtimeReportOutputSchema, @@ -400,15 +280,13 @@ export const runRealtimeReportTool = (env: Env) => if (args.orderBys !== undefined) body.orderBys = args.orderBys; if (args.limit !== undefined) body.limit = args.limit; if (args.offset !== undefined) body.offset = args.offset; - if (args.returnPropertyQuota !== undefined) { + if (args.returnPropertyQuota !== undefined) body.returnPropertyQuota = args.returnPropertyQuota; - } const response = await client.runRealtimeReport( resolveProperty(env, args.property), body, ); - - return RunRealtimeReportOutputSchema.parse({ response }); + return RunRealtimeReportOutputSchema.parse(response); } catch (error) { throw new Error( `Failed to run realtime report: ${error instanceof Error ? error.message : String(error)}`, From 09a23cbdde4c863929b63a23b5285ea329ea4b21 Mon Sep 17 00:00:00 2001 From: yuriassuncx Date: Wed, 24 Jun 2026 18:08:20 -0300 Subject: [PATCH 2/5] fix(google-analytics): wrap boolean fields with fromJson and handle null strings - Apply fromJson() to returnPropertyQuota (all 3 report tools) and FunnelStepSchema.isDirectlyFollowedBy so MCP clients that serialize booleans as JSON strings (e.g. "true") pass validation, consistent with the existing fix for arrays, objects, and numbers. - In fromJson, treat JSON-parsed null as undefined so optional fields that receive the string "null" degrade gracefully instead of failing Zod validation. Co-Authored-By: Claude Sonnet 4.6 --- google-analytics/server/tools/reports.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/google-analytics/server/tools/reports.ts b/google-analytics/server/tools/reports.ts index 7b78f13f..38adec7c 100644 --- a/google-analytics/server/tools/reports.ts +++ b/google-analytics/server/tools/reports.ts @@ -15,7 +15,8 @@ const fromJson = (schema: T) => z.preprocess((val) => { if (typeof val !== "string") return val; try { - return JSON.parse(val); + const parsed = JSON.parse(val); + return parsed === null ? undefined : parsed; } catch { return val; } @@ -98,8 +99,7 @@ export const runReportTool = (env: Env) => .describe( "Optional ISO 4217 currency code for revenue metrics, e.g. 'USD'.", ), - returnPropertyQuota: z - .boolean() + returnPropertyQuota: fromJson(z.boolean()) .optional() .describe( "If true, includes the current GA4 property quota state in the response.", @@ -145,8 +145,7 @@ const FunnelStepSchema = z.object({ filterExpression: fromJson(z.record(z.string(), z.unknown())).describe( "Required. Condition that qualifies users for this step. See https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/FunnelStep#FunnelFilterExpression", ), - isDirectlyFollowedBy: z - .boolean() + isDirectlyFollowedBy: fromJson(z.boolean()) .optional() .describe( "If true, this step must immediately follow the previous step (no intervening events). Defaults to false.", @@ -193,8 +192,7 @@ export const runFunnelReportTool = (env: Env) => offset: fromJson(z.number().int().nonnegative()) .optional() .describe("Row offset for pagination."), - returnPropertyQuota: z - .boolean() + returnPropertyQuota: fromJson(z.boolean()) .optional() .describe("If true, includes current GA4 property quota in response."), }), @@ -260,8 +258,7 @@ export const runRealtimeReportTool = (env: Env) => offset: fromJson(z.number().int().nonnegative()) .optional() .describe("Row offset for pagination."), - returnPropertyQuota: z - .boolean() + returnPropertyQuota: fromJson(z.boolean()) .optional() .describe("If true, includes quota state in the response."), }), From 4b256c054720cf2374512469c5a506b7a1508b9d Mon Sep 17 00:00:00 2001 From: yuriassuncx Date: Wed, 24 Jun 2026 18:12:47 -0300 Subject: [PATCH 3/5] fix(google-analytics): coerce native null to undefined in fromJson preprocessor MCP clients that send null explicitly for absent optional fields (e.g. {"dimensionFilter": null}) were getting a hard Zod type error instead of the field being treated as absent. Add an explicit null check so both native null and the JSON string "null" are treated as undefined. Co-Authored-By: Claude Sonnet 4.6 --- google-analytics/server/tools/reports.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/google-analytics/server/tools/reports.ts b/google-analytics/server/tools/reports.ts index 38adec7c..ff9a127a 100644 --- a/google-analytics/server/tools/reports.ts +++ b/google-analytics/server/tools/reports.ts @@ -13,6 +13,7 @@ import { // types. This preprocessor parses the string before Zod validates it. const fromJson = (schema: T) => z.preprocess((val) => { + if (val === null) return undefined; if (typeof val !== "string") return val; try { const parsed = JSON.parse(val); From 532d1c41b511e05b64835156215644acd80cbafd Mon Sep 17 00:00:00 2001 From: yuriassuncx Date: Thu, 9 Jul 2026 11:54:19 -0300 Subject: [PATCH 4/5] fix(google-analytics): fix null handling in optional fields and align schemas with GA4 API - fromJson(schema).optional() didn't accept null/"null" for optional fields because ZodOptional only short-circuits on undefined before the preprocessor runs; moved .optional() inside fromJson(...) so the coercion to undefined is honored by the resulting schema. - Fixed PropertyQuota's tokensPerProjectPerDay -> tokensPerProjectPerHour (wrong field name silently dropped real quota data). - Added missing fields per the GA4 REST API: PropertySummary.canEdit, ReportingDataAnnotation.annotationDateRange, and ResponseMetaData.dataLossFromOtherRow/emptyReason. Co-Authored-By: Claude Sonnet 5 --- google-analytics/server/lib/schemas.ts | 13 ++- google-analytics/server/tools/reports.ts | 141 +++++++++++------------ 2 files changed, 78 insertions(+), 76 deletions(-) diff --git a/google-analytics/server/lib/schemas.ts b/google-analytics/server/lib/schemas.ts index 746f4576..0d5b90ec 100644 --- a/google-analytics/server/lib/schemas.ts +++ b/google-analytics/server/lib/schemas.ts @@ -25,6 +25,7 @@ export const PropertySummarySchema = z.object({ displayName: z.string(), propertyType: z.string().optional(), parent: z.string().optional(), + canEdit: z.boolean().optional(), }); export const AccountSummarySchema = z.object({ @@ -103,6 +104,12 @@ export const GoogleAdsLinksOutputSchema = z.object({ export const ReportingDataAnnotationSchema = z.object({ name: z.string().optional(), annotationDate: DateSchema.optional(), + annotationDateRange: z + .object({ + startDate: DateSchema.optional(), + endDate: DateSchema.optional(), + }) + .optional(), title: z.string().optional(), description: z.string().optional(), systemGenerated: z.boolean().optional(), @@ -138,9 +145,11 @@ export const RowSchema = z.object({ }); export const ResponseMetaDataSchema = z.object({ + dataLossFromOtherRow: z.boolean().optional(), + schemaRestrictionResponse: z.unknown().optional(), currencyCode: z.string().optional(), timeZone: z.string().optional(), - schemaRestrictionResponse: z.unknown().optional(), + emptyReason: z.string().optional(), subjectToThresholding: z.boolean().optional(), samplingMetadatas: z.array(z.unknown()).optional(), }); @@ -162,7 +171,7 @@ export const PropertyQuotaSchema = z potentiallyThresholdedRequestsPerHour: z .object({ consumed: z.number(), remaining: z.number() }) .optional(), - tokensPerProjectPerDay: z + tokensPerProjectPerHour: z .object({ consumed: z.number(), remaining: z.number() }) .optional(), }) diff --git a/google-analytics/server/tools/reports.ts b/google-analytics/server/tools/reports.ts index ff9a127a..cc538de6 100644 --- a/google-analytics/server/tools/reports.ts +++ b/google-analytics/server/tools/reports.ts @@ -11,6 +11,9 @@ import { // Some MCP clients serialize arrays/objects as JSON strings instead of native // types. This preprocessor parses the string before Zod validates it. +// `.optional()` must be applied to the schema passed in here (not chained +// after fromJson(...)) so that a `null` value — which the preprocessor +// coerces to `undefined` — is actually accepted by the resulting schema. const fromJson = (schema: T) => z.preprocess((val) => { if (val === null) return undefined; @@ -67,44 +70,36 @@ export const runReportTool = (env: Env) => dateRanges: fromJson(z.array(DateRangeSchema).min(1)).describe( "One or more date ranges to include in the report.", ), - dimensions: fromJson(z.array(DimensionSchema)) - .optional() - .describe( - "Dimensions to group results by, e.g. [{ name: 'sessionSource' }].", - ), + dimensions: fromJson(z.array(DimensionSchema).optional()).describe( + "Dimensions to group results by, e.g. [{ name: 'sessionSource' }].", + ), metrics: fromJson(z.array(MetricSchema).min(1)).describe( "Metrics to aggregate, e.g. [{ name: 'activeUsers' }].", ), - dimensionFilter: fromJson(FilterExpressionSchema) - .optional() - .describe("Optional filter to restrict dimension values."), - metricFilter: fromJson(FilterExpressionSchema) - .optional() - .describe("Optional filter to restrict metric values."), - orderBys: fromJson(z.array(OrderBySchema)) - .optional() - .describe("Optional ordering for returned rows."), - limit: fromJson(z.number().int().positive()) - .optional() - .describe( - "Maximum number of rows to return. Defaults to 10,000; max 250,000.", - ), - offset: fromJson(z.number().int().nonnegative()) - .optional() - .describe( - "Row offset for pagination (0-based). Use with limit to page through results.", - ), + dimensionFilter: fromJson(FilterExpressionSchema.optional()).describe( + "Optional filter to restrict dimension values.", + ), + metricFilter: fromJson(FilterExpressionSchema.optional()).describe( + "Optional filter to restrict metric values.", + ), + orderBys: fromJson(z.array(OrderBySchema).optional()).describe( + "Optional ordering for returned rows.", + ), + limit: fromJson(z.number().int().positive().optional()).describe( + "Maximum number of rows to return. Defaults to 10,000; max 250,000.", + ), + offset: fromJson(z.number().int().nonnegative().optional()).describe( + "Row offset for pagination (0-based). Use with limit to page through results.", + ), currencyCode: z .string() .optional() .describe( "Optional ISO 4217 currency code for revenue metrics, e.g. 'USD'.", ), - returnPropertyQuota: fromJson(z.boolean()) - .optional() - .describe( - "If true, includes the current GA4 property quota state in the response.", - ), + returnPropertyQuota: fromJson(z.boolean().optional()).describe( + "If true, includes the current GA4 property quota state in the response.", + ), }), outputSchema: RunReportOutputSchema, execute: async ({ context: args }) => { @@ -146,11 +141,9 @@ const FunnelStepSchema = z.object({ filterExpression: fromJson(z.record(z.string(), z.unknown())).describe( "Required. Condition that qualifies users for this step. See https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/FunnelStep#FunnelFilterExpression", ), - isDirectlyFollowedBy: fromJson(z.boolean()) - .optional() - .describe( - "If true, this step must immediately follow the previous step (no intervening events). Defaults to false.", - ), + isDirectlyFollowedBy: fromJson(z.boolean().optional()).describe( + "If true, this step must immediately follow the previous step (no intervening events). Defaults to false.", + ), withinDurationFromPriorStep: z .string() .optional() @@ -177,25 +170,25 @@ export const runFunnelReportTool = (env: Env) => dateRanges: fromJson(z.array(DateRangeSchema).min(1)).describe( "One or more date ranges to include in the report.", ), - funnelBreakdown: fromJson(z.record(z.string(), z.unknown())) - .optional() - .describe( - "Optional FunnelBreakdown — adds a sub-dimension to the funnel table rows.", - ), - funnelNextAction: fromJson(z.record(z.string(), z.unknown())) - .optional() - .describe( - "Optional FunnelNextAction — adds a next-action dimension showing what users do after abandoning a step.", - ), - limit: fromJson(z.number().int().positive()) - .optional() - .describe("Maximum number of rows to return."), - offset: fromJson(z.number().int().nonnegative()) - .optional() - .describe("Row offset for pagination."), - returnPropertyQuota: fromJson(z.boolean()) - .optional() - .describe("If true, includes current GA4 property quota in response."), + funnelBreakdown: fromJson( + z.record(z.string(), z.unknown()).optional(), + ).describe( + "Optional FunnelBreakdown — adds a sub-dimension to the funnel table rows.", + ), + funnelNextAction: fromJson( + z.record(z.string(), z.unknown()).optional(), + ).describe( + "Optional FunnelNextAction — adds a next-action dimension showing what users do after abandoning a step.", + ), + limit: fromJson(z.number().int().positive().optional()).describe( + "Maximum number of rows to return.", + ), + offset: fromJson(z.number().int().nonnegative().optional()).describe( + "Row offset for pagination.", + ), + returnPropertyQuota: fromJson(z.boolean().optional()).describe( + "If true, includes current GA4 property quota in response.", + ), }), outputSchema: RunFunnelReportOutputSchema, execute: async ({ context: args }) => { @@ -238,30 +231,30 @@ export const runRealtimeReportTool = (env: Env) => .describe( "GA4 Property identifier — 'properties/1234567' or just '1234567'. Falls back to the default propertyId configured for this integration when omitted.", ), - dimensions: fromJson(z.array(DimensionSchema)) - .optional() - .describe("Dimensions to group results by."), + dimensions: fromJson(z.array(DimensionSchema).optional()).describe( + "Dimensions to group results by.", + ), metrics: fromJson(z.array(MetricSchema).min(1)).describe( "Metrics to aggregate.", ), - dimensionFilter: fromJson(FilterExpressionSchema) - .optional() - .describe("Optional filter to restrict dimension values."), - metricFilter: fromJson(FilterExpressionSchema) - .optional() - .describe("Optional filter to restrict metric values."), - orderBys: fromJson(z.array(OrderBySchema)) - .optional() - .describe("Optional ordering for returned rows."), - limit: fromJson(z.number().int().positive()) - .optional() - .describe("Maximum number of rows to return."), - offset: fromJson(z.number().int().nonnegative()) - .optional() - .describe("Row offset for pagination."), - returnPropertyQuota: fromJson(z.boolean()) - .optional() - .describe("If true, includes quota state in the response."), + dimensionFilter: fromJson(FilterExpressionSchema.optional()).describe( + "Optional filter to restrict dimension values.", + ), + metricFilter: fromJson(FilterExpressionSchema.optional()).describe( + "Optional filter to restrict metric values.", + ), + orderBys: fromJson(z.array(OrderBySchema).optional()).describe( + "Optional ordering for returned rows.", + ), + limit: fromJson(z.number().int().positive().optional()).describe( + "Maximum number of rows to return.", + ), + offset: fromJson(z.number().int().nonnegative().optional()).describe( + "Row offset for pagination.", + ), + returnPropertyQuota: fromJson(z.boolean().optional()).describe( + "If true, includes quota state in the response.", + ), }), outputSchema: RunRealtimeReportOutputSchema, execute: async ({ context: args }) => { From 49233023bad6df804c5a7d04faa252ce1d7037d8 Mon Sep 17 00:00:00 2001 From: yuriassuncx Date: Thu, 9 Jul 2026 12:08:18 -0300 Subject: [PATCH 5/5] fix(google-analytics): restore GA4 field documentation comments in schemas.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The input-validation fix accidentally dropped JSDoc comments documenting resource-name formats and enum values for GA4 API fields. Restore them — they're useful reference for anyone reading the schema, independent of the response-wrapper removal. Co-Authored-By: Claude Sonnet 5 --- google-analytics/server/lib/schemas.ts | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/google-analytics/server/lib/schemas.ts b/google-analytics/server/lib/schemas.ts index 0d5b90ec..2eeb9c91 100644 --- a/google-analytics/server/lib/schemas.ts +++ b/google-analytics/server/lib/schemas.ts @@ -10,8 +10,10 @@ import { z } from "zod"; // ── Shared primitives ──────────────────────────────────────────────────────── +/** ISO-8601 timestamp string (e.g. "2024-01-15T10:30:00Z"). */ const Timestamp = z.string(); +/** Represents a date used in annotation. */ const DateSchema = z.object({ year: z.number().int().optional(), month: z.number().int().optional(), @@ -21,15 +23,20 @@ const DateSchema = z.object({ // ── Admin API — Account Summaries ──────────────────────────────────────────── export const PropertySummarySchema = z.object({ + /** Resource name, e.g. "properties/123456". */ property: z.string(), displayName: z.string(), + /** e.g. "PROPERTY_TYPE_ORDINARY" | "PROPERTY_TYPE_ROLLUP" | "PROPERTY_TYPE_SUBPROPERTY" */ propertyType: z.string().optional(), + /** Parent resource name, e.g. "accounts/123456". */ parent: z.string().optional(), canEdit: z.boolean().optional(), }); export const AccountSummarySchema = z.object({ + /** Resource name, e.g. "accountSummaries/123456". */ name: z.string(), + /** Account resource name, e.g. "accounts/123456". */ account: z.string().optional(), displayName: z.string().optional(), propertySummaries: z.array(PropertySummarySchema).optional(), @@ -42,7 +49,9 @@ export const AccountSummariesOutputSchema = z.object({ // ── Admin API — Property ───────────────────────────────────────────────────── export const PropertySchema = z.object({ + /** Resource name, e.g. "properties/123456". */ name: z.string().optional(), + /** Parent resource, e.g. "accounts/123456" or "properties/123" for sub-properties. */ parent: z.string().optional(), createTime: Timestamp.optional(), updateTime: Timestamp.optional(), @@ -50,7 +59,9 @@ export const PropertySchema = z.object({ industryCategory: z.string().optional(), timeZone: z.string().optional(), currencyCode: z.string().optional(), + /** e.g. "GOOGLE_ANALYTICS_STANDARD" | "GOOGLE_ANALYTICS_360" */ serviceLevel: z.string().optional(), + /** e.g. "PROPERTY_TYPE_ORDINARY" | "PROPERTY_TYPE_ROLLUP" | "PROPERTY_TYPE_SUBPROPERTY" */ propertyType: z.string().optional(), account: z.string().optional(), deleteTime: Timestamp.optional(), @@ -60,21 +71,28 @@ export const PropertySchema = z.object({ // ── Admin API — Custom Dimensions & Metrics ────────────────────────────────── export const CustomDimensionSchema = z.object({ + /** Resource name, e.g. "properties/123/customDimensions/456". */ name: z.string().optional(), + /** The parameter name used in events / user properties. */ parameterName: z.string(), displayName: z.string().optional(), description: z.string().optional(), + /** "EVENT" | "USER" | "ITEM" */ scope: z.string().optional(), disallowAdsPersonalization: z.boolean().optional(), }); export const CustomMetricSchema = z.object({ + /** Resource name. */ name: z.string().optional(), parameterName: z.string(), displayName: z.string().optional(), description: z.string().optional(), + /** e.g. "STANDARD" | "CURRENCY" | "FEET" | "METERS" | "KILOMETERS" | "MILES" | "MILLISECONDS" | "SECONDS" | "MINUTES" | "HOURS" */ measurementUnit: z.string().optional(), + /** "EVENT" | "ITEM" */ scope: z.string().optional(), + /** e.g. ["COST_DATA", "REVENUE_DATA"] */ restrictedMetricType: z.array(z.string()).optional(), }); @@ -86,7 +104,9 @@ export const CustomDimensionsAndMetricsOutputSchema = z.object({ // ── Admin API — Google Ads Links ───────────────────────────────────────────── export const GoogleAdsLinkSchema = z.object({ + /** Resource name. */ name: z.string().optional(), + /** Google Ads Customer ID (without hyphens). */ customerId: z.string().optional(), canManageClients: z.boolean().optional(), adsPersonalizationEnabled: z.boolean().optional(), @@ -102,6 +122,7 @@ export const GoogleAdsLinksOutputSchema = z.object({ // ── Admin API — Property Annotations ───────────────────────────────────────── export const ReportingDataAnnotationSchema = z.object({ + /** Resource name. */ name: z.string().optional(), annotationDate: DateSchema.optional(), annotationDateRange: z @@ -113,6 +134,7 @@ export const ReportingDataAnnotationSchema = z.object({ title: z.string().optional(), description: z.string().optional(), systemGenerated: z.boolean().optional(), + /** "RED" | "ORANGE" | "YELLOW" | "GREEN" | "BLUE" | "PURPLE" | "PINK" */ color: z.string().optional(), }); @@ -128,6 +150,7 @@ export const DimensionHeaderSchema = z.object({ export const MetricHeaderSchema = z.object({ name: z.string(), + /** e.g. "TYPE_INTEGER" | "TYPE_FLOAT" | "TYPE_SECONDS" | "TYPE_MILLISECONDS" | "TYPE_MINUTES" | "TYPE_HOURS" | "TYPE_STANDARD" | "TYPE_CURRENCY" | "TYPE_FEET" | "TYPE_MILES" | "TYPE_METERS" | "TYPE_KILOMETERS" */ type: z.string().optional(), }); @@ -154,6 +177,7 @@ export const ResponseMetaDataSchema = z.object({ samplingMetadatas: z.array(z.unknown()).optional(), }); +/** Quota state returned when `returnPropertyQuota: true`. */ export const PropertyQuotaSchema = z .object({ tokensPerDay: z @@ -189,6 +213,7 @@ export const RunReportOutputSchema = z.object({ rowCount: z.number().optional(), metadata: ResponseMetaDataSchema.optional(), propertyQuota: PropertyQuotaSchema.optional(), + /** Always "analyticsData#runReport". */ kind: z.string().optional(), }); @@ -203,6 +228,7 @@ export const RunRealtimeReportOutputSchema = z.object({ minimums: z.array(RowSchema).optional(), rowCount: z.number().optional(), propertyQuota: PropertyQuotaSchema.optional(), + /** Always "analyticsData#runRealtimeReport". */ kind: z.string().optional(), }); @@ -213,6 +239,10 @@ const FunnelResponseMetaDataSchema = z.object({ schemaRestrictionResponse: z.unknown().optional(), }); +/** + * Funnel response rows share the same Row structure (dimensionValues + + * metricValues), grouped in funnelTable and funnelVisualization sub-objects. + */ const FunnelSubReportSchema = z.object({ dimensionHeaders: z.array(DimensionHeaderSchema).optional(), metricHeaders: z.array(MetricHeaderSchema).optional(), @@ -228,5 +258,6 @@ export const RunFunnelReportOutputSchema = z.object({ funnelTable: FunnelSubReportSchema.optional(), funnelVisualization: FunnelSubReportSchema.optional(), propertyQuota: PropertyQuotaSchema.optional(), + /** Always "analyticsData#runFunnelReport". */ kind: z.string().optional(), });