# Compare Search Analytics — Google Search Console

> Compare Google Search Console traffic between two date ranges for one property and return the deltas. Fetches both periods in parallel, joins the rows on their dimension keys, and reports per-row and total clicks, impressions, ctr and…

- Key: `google_search_console-compare-search-analytics`
- Type: Action (Read-only)
- Version: 0.0.1
- App: Google Search Console (`google_search_console`) — https://pipedream.com/apps/google-search-console.md
- This page (HTML): https://pipedream.com/apps/google-search-console/actions/compare-search-analytics
- Hints: read-only · open-world
- Source: https://github.com/PipedreamHQ/pipedream/blob/master/components/google_search_console/actions/compare-search-analytics/compare-search-analytics.mjs

## Description

Compare Google Search Console traffic between two date ranges for one property and return the deltas. Fetches both periods in parallel, joins the rows on their dimension keys, and reports per-row and total `clicks`, `impressions`, `ctr` and `position` change — the period-over-period arithmetic is done for you.

**Use for** any question that compares two date ranges: month over month, quarter over quarter, year over year, "which queries gained or lost the most clicks", "did mobile grow", "did that update hurt us". Use **Query Search Analytics** for a single date range, for paging past 5000 rows, and for the `hour` and `searchAppearance` dimensions this tool does not accept. Property-level vs page-level position questions also need two **Query Search Analytics** calls with different `aggregationType` values.

**Returns** `{ current_period, previous_period, totals, rows, row_count, has_more, truncated, note }`. `totals` carries `current`, `previous`, `delta` and `pct_change` for the whole period, computed from every fetched row rather than just the returned ones. Each row is `{ keys, current, previous, delta, pct_change }`.

**Reading the deltas.** A key present in only one period gets zeros for the other, so new and lost queries both show up — but its `delta.ctr` and `delta.position` are `null`, because the missing period has no CTR or position to compare against (`delta.clicks` and `delta.impressions` are still real numbers). `pct_change` is `null` where the previous period was zero — not 0 and not infinity. `ctr` is a 0-1 fraction (`0.1428` means 14.3%) and `position` is 1-indexed where lower is better, so a NEGATIVE position delta is an improvement; both are impression-weighted, so never re-average them across rows. Query rows always understate the real total because Google omits anonymized (rare) queries — compare with no dimensions, or by `date`, for true totals. `truncated` is true when either period hit the internal 5000-row cap, so rows and totals may be incomplete: narrow the range or add a filter. `has_more` is a different signal — it only means the join produced more rows than `rowLimit`. `note` warns about anonymized queries when relevant, otherwise it is `null`.

**Mistakes.** "The previous period" means the same number of days immediately before the current period: for 2026-08-01..2026-08-28 (28 days) that is 2026-07-04..2026-07-31. "The same period last year" means both dates shifted back one year. Grouping Discover by `query` returns a 400 — Discover has no `query` dimension. Comparing a range that ends today makes the current period look artificially low, because data is final only after about 2-3 days unless `dataState` is `all`. `filterValue` (with `filterDimension`/`filterOperator`) is applied identically to both periods and wins over `advancedDimensionFilters`; the equivalent prop on **Query Search Analytics** is named `subdomainFilter` for backwards compatibility.

**Example.** `siteUrl="sc-domain:example.com"`, `currentStartDate="2026-08-01"`, `currentEndDate="2026-08-28"`, `previousStartDate="2026-07-04"`, `previousEndDate="2026-07-31"`, `dimensions=["query"]`, `sortBy="clicks_delta"` returns `totals: { current: { clicks: 74, ... }, previous: { clicks: 68, ... }, delta: { clicks: 6, ... }, pct_change: { clicks: 0.0882, ... } }` and rows such as `{ keys: ["example brand"], current: { clicks: 41, ... }, previous: { clicks: 33, ... }, delta: { clicks: 8, ... } }`.

[See the documentation](https://developers.google.com/webmaster-tools/v1/searchanalytics/query)

## Props

| Prop | Type | Required | Description |
|---|---|---|---|
| `siteUrl` | `string` | Yes | Exact property identifier from List Sites — sc-domain:example.com for a domain property, or a URL-prefix such as https://www.example.com/ (trailing slash; scheme and subdomain must match exactly, or the call 403s). Copy it verbatim, never construct it. For traffic questions prefer the domain property when one exists: it covers all subdomains and protocols. |
| `currentStartDate` | `string` | Yes | First day of the RECENT period, inclusive, in YYYY-MM-DD Pacific Time — e.g. 2026-08-01. |
| `currentEndDate` | `string` | Yes | Last day of the RECENT period, inclusive — e.g. 2026-08-28. Data is final only after 2-3 days, so avoid ending on today unless dataState is all. |
| `previousStartDate` | `string` | Yes | First day of the BASELINE period, inclusive. For "the previous period" use the same number of days immediately before the current one — for 2026-08-01..2026-08-28 that is 2026-07-04. For "the same period last year", shift currentStartDate back a year: 2025-08-01. |
| `previousEndDate` | `string` | Yes | Last day of the BASELINE period, inclusive. For "the previous period" it is the day before currentStartDate — for a current period starting 2026-08-01 that is 2026-07-31. For "the same period last year", shift currentEndDate back a year. |
| `dimensions` | `string[]` | No | How to group the compared rows; each row's keys lines up positionally with this list. Leave empty to compare one totals row per period — the right choice for "how did traffic change overall". Use query for which queries gained or lost, page for page-level movement, device for mobile vs desktop, country for market shifts, date for a day-by-day pair-up. hour and searchAppearance are not supported here — use Query Search Analytics. |
| `searchType` | `string` | No | Which Google surface to report on. web (default) is normal Google Search; discover is the Discover feed (no query dimension); googleNews is news.google.com, news is the News tab of Google Search. Sent to the API as type. |
| `dataState` | `string` | No | Which data to include, applied to both periods. final (default) returns only finalized data, which lags roughly 2-3 days. all also includes the most recent, not-yet-final days — use it only when the user explicitly wants recent or partial numbers; it makes a current period ending today look incomplete rather than absent. |
| `filterDimension` | `string` | No | Dimension the single-filter shortcut applies to; default page. Filtering does not require grouping by the same dimension — you can filter by page while grouping by query. page expressions match the FULL URL, scheme and host included, not a path. |
| `filterOperator` | `string` | No | How the filter value is compared. String comparison is case-insensitive. includingRegex/excludingRegex use RE2 syntax (no lookahead/lookbehind). Default contains. |
| `filterValue` | `string` | No | The value to filter on, for ANY dimension — not just subdomains (Query Search Analytics keeps the legacy key subdomainFilter). Combined with filterDimension and filterOperator into one filter, e.g. filterDimension: page, filterOperator: contains, value https://www.example.com/blog/. page expressions match the FULL URL, scheme and host included, not a path. Setting this makes advancedDimensionFilters ignored. |
| `advancedDimensionFilters` | `string` | No | JSON for multi-condition filtering, used only when the single-filter shortcut is empty. Accepts either a bare array of filters, which is ANDed into one group — e.g. [{"dimension":"country","operator":"equals","expression":"usa"},{"dimension":"device","operator":"equals","expression":"MOBILE"}] — or the raw API dimensionFilterGroups array, e.g. [{"groupType":"and","filters":[{"dimension":"page","operator":"contains","expression":"/blog/"}]}]. The API only supports groupType: "and"; there is no OR. Regex operators use RE2. |
| `sortBy` | `string` | No | How to order the returned rows; defaults to clicks_delta. The *_delta options sort by ABSOLUTE change, so the biggest gains and losses both appear at the top — read the sign of delta to tell them apart. Under ctr_delta and position_delta, rows whose delta is null (a key present in only one period) sort last, so use clicks_delta to surface new and lost queries. current_clicks sorts by current-period clicks descending — right for "top queries, with their change". |
| `rowLimit` | `integer` | No | How many joined rows to return, after sorting; defaults to 50. Caps the OUTPUT only — up to 5000 rows per period are always fetched, so the totals cover everything even when the row list is short, unless truncated is true. has_more means the join produced more rows than were returned. |

## Run it

**MCP**

```ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { PipedreamClient } from "@pipedream/sdk"

const pd = new PipedreamClient({
  projectId: process.env.PIPEDREAM_PROJECT_ID!,
  clientId: process.env.PIPEDREAM_CLIENT_ID!,
  clientSecret: process.env.PIPEDREAM_CLIENT_SECRET!,
  projectEnvironment: "production",
})

const accessToken = await pd.rawAccessToken

const transport = new StreamableHTTPClientTransport(
  new URL("https://remote.mcp.pipedream.net/v3"),
  {
    requestInit: {
      headers: {
        Authorization: `Bearer ${accessToken}`,
        "x-pd-project-id": process.env.PIPEDREAM_PROJECT_ID!,
        "x-pd-environment": "production",
        "x-pd-external-user-id": "{external_user_id}", // any stable ID for this user in your system
        "x-pd-app-slug": "google_search_console",
      },
    },
  },
)

const mcp = new Client({ name: "my-agent", version: "1.0.0" })
await mcp.connect(transport)

const { tools } = await mcp.listTools()

// listTools() hands your model this tool's input schema, so it can
// fill the arguments itself:
const result = await mcp.callTool({
  name: "google_search_console-compare-search-analytics",
  arguments: {
    siteUrl: "Property (siteUrl)",
    currentStartDate: "Current Period Start Date (YYYY-MM-DD)",
  },
})
```

**TypeScript**

```ts
import { PipedreamClient } from "@pipedream/sdk"

const pd = new PipedreamClient({
  projectId: process.env.PIPEDREAM_PROJECT_ID!,
  clientId: process.env.PIPEDREAM_CLIENT_ID!,
  clientSecret: process.env.PIPEDREAM_CLIENT_SECRET!,
  projectEnvironment: "production",
})

const result = await pd.actions.run({
  id: "google_search_console-compare-search-analytics",
  externalUserId: "{external_user_id}", // any stable ID for this user in your system
  configuredProps: {
    google_search_console: { authProvisionId: "apn_xxxxxxx" },
    siteUrl: "Property (siteUrl)",
    currentStartDate: "Current Period Start Date (YYYY-MM-DD)",
  },
})

console.log(result)
```

**cURL**

```bash
curl -X POST https://api.pipedream.com/v1/connect/{project_id}/actions/run \
  -H "Content-Type: application/json" \
  -H "X-PD-Environment: production" \
  -H "Authorization: Bearer {access_token}" \
  -d '{
    "external_user_id": "{external_user_id}",
    "id": "google_search_console-compare-search-analytics",
    "configured_props": {
      "google_search_console": { "authProvisionId": "apn_xxxxxxx" },
      "siteUrl": "Property (siteUrl)",
      "currentStartDate": "Current Period Start Date (YYYY-MM-DD)"
    }
  }'
```

---

- App: https://pipedream.com/apps/google-search-console.md · All apps: https://pipedream.com/apps
