TrendingConnect Google Analytics to an AI assistant, and let it do the reporting.
How-To Guides

Connect Google Analytics to an AI assistant, and let it do the reporting.

The whole path, in order: the Google Cloud project, the credential, the connection, and then the part that actually pays for itself. One scheduled pull becomes a dashboard, a cohort grid the interface cannot draw, a written weekly summary, and a Slack alert that stays quiet until something real moves.

MSMikołaj Salecki, portrait
Editor-in-chief
Aug 1, 2026·21 min read
A cropped plaster hand holding one long thin tube that runs across a dotted measurement grid into the cut-open face of a classical head filled with clouds and buildings, a solid brand-blue disk behind it
The pipe takes an afternoon. What comes out of it is the part worth planning.Illustration: Mediovsky · generated with AI
TL;DR
  • Setup is a Cloud project, two APIs, a credential, and one step people miss: granting that credential access inside Analytics. [20]
  • Google's official Analytics MCP server is read-only by scope, not by promise, and ships seven tools. [1]
  • Editors are not interchangeable: VS Code reads a servers key, Cursor reads mcpServers. [8][9]
  • MCP is for asking while you watch. Code is for anything that runs on a Monday without you.
  • One fetch.mjs writes data.json. The dashboard, the email, and the alert all read it and call nothing. [21]
  • Cohort retention is one cohortSpec. The interface has no screen for it. [4]
  • Funnels work, but only in v1alpha, and Google says to expect breaking changes. [5]
  • Every response says whether it was sampled, thresholded, or truncated. Almost nothing reads it. [6]
  • Budget: 200,000 core tokens a day, 40,000 an hour, 10 concurrent, on a standard property. [2]
  • Schedule against the data, not the calendar: processing can take 24 to 48 hours. [11]

Most guides to this stop at the demo. You connect a server, you type a question, an assistant answers, and the article ends on how remarkable that is. It is remarkable for about a day. Then you want the same answer every Monday without typing anything, you want it to reach people who will never open a dashboard, and you want to know whether the number is even complete.

This is the whole path instead. Setup from an empty Google Cloud console, the choice between the two ways in, how to ask so the answer is reproducible, and then four things worth building on top. Only one of them is a dashboard.

00What you are actually building

Before the setup, the destination, because it changes which decisions matter.

1

One scheduled pull

A small script authenticates, sends a single request, computes the comparisons, and writes one file called data.json. This is the only thing that ever talks to Google.

Costs quota
2

Four things read that file

An HTML dashboard. A cohort grid and a funnel the Analytics interface will not draw for you. A written weekly summary that goes out by email. A Slack alert that stays silent unless something real moved.

Costs nothing
3

A human signs the ones that leave the building

The dashboard and the alert can run unattended. Anything a client reads gets a person in front of it, because the week a tracking change breaks is the week the report looks most dramatic.

Judgment

That split is the whole architecture. Adding a fifth destination later costs a reader, not another API call, so the quota bill does not move.

01Google Cloud, from an empty console

What you need before you start

An Analytics property you can administer, a Google account, and about an hour. On the machine: Node 18 or newer for the scripts, and pipx for the server, which Google's own instructions require. [1] You also need your property ID, the numeric one: open Analytics, then Admin, then Property settings, and it sits at the top right. It is not the G- measurement ID from your tag, which is a different thing entirely.

Analytics data does not come out of Analytics. It comes out of a Google Cloud project that you own, which is why this step exists even though nothing about it feels like marketing.

Create a project, then enable exactly two APIs in it: the Analytics Admin API, which lists what accounts and properties exist, and the Analytics Data API, which returns the numbers. [1] Google's own server needs both. [1]

Then the credential, and the choice here is the one that bites later.

Application Default Credentials Service account
What it is your own Google login, cached locally a robot identity with its own email address
Sees exactly what you see in Analytics exactly what you grant it, and nothing else
Right for your laptop, exploring anything scheduled or shared
Breaks when you rotate access or someone else runs it never, unless you remove its access

On a laptop, Application Default Credentials are the sane default precisely because they are not special. Google's own line asks for the read-only Analytics scope: [1]

gcloud auth application-default login \
  --scopes https://www.googleapis.com/auth/analytics.readonly,\
https://www.googleapis.com/auth/cloud-platform \
  --client-id-file=YOUR_CLIENT_JSON_FILE

The moment anything runs on a schedule, switch to a service account. And here is the step that quietly costs people an afternoon.

The step everyone skips

A service account is a Google Cloud identity. It is not a Google Analytics user until you make it one. Copy its email address, open Admin, then Property access management in Analytics, and add it with the Viewer role, which can "see settings and data" and nothing more. [20] Skip this and every credential is valid, every API is enabled, and every request comes back empty or denied. Nothing in the error tells you which of the two systems is unhappy.

A cracked plaster archway framing a grey monochrome apartment block and clouds, with a small keyhole outlined in brand blue, beside a plaster hand gripping a key far larger than the keyhole, over a solid brand-blue floor
Two locks, not one. The credential opens Cloud, the role opens the property.Illustration: Mediovsky · generated with AI

02Connecting the assistant

Google publishes an official server for this, and its own title for it is Google Analytics MCP Server (Experimental). [1] Read that label as a version number rather than a warning. The Model Context Protocol it speaks is simply the standard way an assistant reaches an outside system.

Seven tools, in three groups. get_account_summaries, get_property_details, and list_google_ads_links tell the assistant what exists. run_report, run_funnel_report, and get_custom_dimensions_and_metrics do the reporting. run_realtime_report covers the live view. [1]

Every one of them reads. Nothing creates a property, edits a key event, or touches a data stream, and the guarantee is stronger than a tool list, because the README requires credentials carrying the read-only scope https://www.googleapis.com/auth/analytics.readonly. [1] The permission forbids the write. On any community server, check that same thing: a tool list is a claim about behavior, a scope is a limit on capability, and anything asking for more than read access to produce a report is asking for permission it does not need.

The command comes from Google's README, which is the version to trust: [1]

claude mcp add analytics-mcp \
  --scope user \
  -e "GOOGLE_APPLICATION_CREDENTIALS=PATH_TO_CREDENTIALS_JSON" \
  -e "GOOGLE_PROJECT_ID=YOUR_PROJECT_ID" \
  -- pipx run analytics-mcp

One wrinkle worth knowing before it costs you twenty minutes: the README's prose recommends a GOOGLE_CLOUD_PROJECT variable while the JSON example beside it sets GOOGLE_PROJECT_ID. [1] If the server starts but cannot resolve a project, check that first.

Every serious editor speaks this protocol now, but the config files are not interchangeable and the failure is silent.

Editor File Top-level key Notes
Claude Code .mcp.json, or the CLI written for you --scope picks local, project, or user [7]
VS Code .vscode/mcp.json servers MCP: Add Server writes it through a guided flow [8]
Cursor .cursor/mcp.json mcpServers stdio, SSE, and streamable HTTP [9]
Gemini CLI ~/.gemini/settings.json mcpServers the setup Google's README documents first [1]

Paste a Cursor block into VS Code and nothing happens, because VS Code looks for servers and finds mcpServers. [8][9] No error, no server, no clue. When it is wired, ask it what properties you have. If it lists them, both locks are open.

03Two ways in, and when each one is right

You now have a connection, and there are two ways to use it. This is not a rivalry. It splits on one question: is a human watching?

MCP, in a conversation The Data API, in code
Good at open questions, iteration, following a hunch scheduled pulls, versioned logic, anything reused
Time to first answer minutes an afternoon, including auth
Who approves the call you, in the session nobody. It runs at 6 a.m.
Where the logic lives in the conversation, gone tomorrow in a file, in git, reviewable
Failure mode a slightly wrong dimension, which you notice the job dies quietly and you find out Friday

The fourth row decides it. A question you asked once is not an asset. The moment you want the same numbers next week, in the same shape, the work belongs in code, because that is the only place a definition survives being forgotten.

So the sequence is: explore in MCP until you know what the report should say, then move that settled request into a file. Everything in section 06 depends on having done the first half honestly.

Both spend from one published budget. A standard property gets 200,000 core tokens per day and 40,000 per hour with 10 concurrent requests, while a single Cloud project is separately capped at 14,000 core tokens per property per hour. Analytics 360 raises every token ceiling tenfold, to 2,000,000 a day, and lifts concurrency to 50. [2] Tokens are charged by query complexity rather than request count, which surprises people: one wide report can cost more than a dozen narrow ones.

04Asking so the answer is reproducible

Connected, you can type anything and get an answer. The trouble is that a good answer and a lucky one look identical.

Which channels did best last week?

It will answer. It will also pick a date range, guess which of several channel dimensions you meant, and choose its own row limit, and none of those three decisions appears in the reply. Ask again next week and you may get a different set of choices, which is fine while you are exploring and fatal the moment anything depends on it.

Four things it cannot guess, so you supply them:

Property 284916037. Compare 2026-07-21 to 2026-07-27 against
2026-07-14 to 2026-07-20.

Dimension sessionDefaultChannelGroup, metric engagedSessions,
ordered by engagedSessions descending, limit 5.

Report the ResponseMetaData with the rows.

Property, both windows, API names, limit. Names matter because they are their own vocabulary, not the labels you see on screen: eventName, pagePath, deviceCategory, country, defaultChannelGroup. [12] Ask for "channels" and you get a plausible guess. Ask for sessionDefaultChannelGroup and you get the column you meant.

The last line is the one people leave off, and it is what turns a confident table into a checkable one. Section 05 is about why.

Here is what that prompt became, and what came back.

A two-column console: on the left the JSON request with two dateRanges, a sessionDefaultChannelGroup dimension, an engagedSessions metric and limit 5; on the right the resulting ranked channel table with an (other) row, and beneath it the returned metadata block showing dataLossFromOtherRow set to true
The prompt above became the request on the left. The sixth row exists because the limit was 5, and the chip says so out loud. Sample data.Dashboard: Mediovsky · rendered from sample data

Two structural details are worth stealing. dateRanges takes an array, so two entries give you a period comparison in one response instead of two calls you have to align yourself. And limit is a real decision: Google returns at most 250,000 rows per request whatever you ask for, and 10,000 if you do not specify. [10] A small limit is usually right, and it is also what creates the (other) row.

Questions worth asking, once it works:

  • Which landing pages lost the most organic sessions week on week, and by how much?
  • Which channel's engaged-session rate moved most, ignoring anything under 500 sessions?
  • What are the top 20 events by count, and which are new since last month?
  • Which pages have high traffic and no key events at all?
  • Break revenue down by device, and tell me what the data cannot explain.

That last clause is a habit worth building. A model asked what it cannot determine will usually tell you, and the answer is often the most useful line in the reply.

05Knowing when the number is lying

Every report comes back with a metadata block describing the conditions it was produced under, and virtually every tool throws it away while keeping the rows. [6] Four fields decide whether a number means what its label says.

Field Google's description What it means for you
subjectToThresholding "If true, this report is subject to thresholding and only returns data that meets minimum aggregation thresholds." [6] Rows were withheld for privacy. Your total is incomplete, and no error was raised
dataLossFromOtherRow "If true, indicates some buckets of dimension combinations are rolled into "(other)" row." [6] A ranking on this is a ranking of what survived, not of everything
samplingMetadatas "If this report results is sampled, this describes the percentage of events used in this report." [6] Estimates, not counts. The rate is computable per date range
emptyReason "If empty reason is specified, the report is empty for this reason." [6] The difference between zero and no answer, which a chart cannot show

Sampling is the one that gives you a number rather than a boolean: samplesReadCount over samplingSpaceSize is your rate. [6] Only one of the three conditions is entirely out of your hands.

Thresholding is privacy machinery, applied "to prevent anyone viewing a report or exploration from inferring the identity or sensitive information of individual users," and it engages around demographics, audiences built on them, and search-query data with too few users. [13] You cannot negotiate: "Data thresholds are system defined. You can't adjust them." [13] A wider date range makes it less likely.

Sampling is a quota. Google puts the event-level query limit at 10 million events on a standard property, with Analytics 360 running to 1 billion from a default of 100 million per query. [14] Shorter windows and fewer dimensions keep you under it.

The (other) row is cardinality: "a row that appears in a report, exploration, or Data API response when the number of rows in a table exceeds the table's row limit." [15] Google warns that "any dimension with more than 500 values should be considered a high-cardinality dimension," and its advice is blunt: use existing dimensions before creating custom ones, and never build one that gives each user a distinct identifier. [15]

A tall stack of thin plaster tablets, crisp and individually separated at the top, fusing into one solid undifferentiated block below a horizontal brand-blue rule, with plaster busts above
Above the line, rows you can name. Below it, one line called (other), and no way back.Illustration: Mediovsky · generated with AI

All three vanish in the raw export, which is the honest escape hatch when a figure has to be complete rather than indicative. That path has its own ceiling: a standard property's daily BigQuery export stops at 1 million events per day, and Google pauses it rather than catching up if you consistently exceed it. [16] It is the same reasoning behind treating the interface as a privacy-filtered view and the warehouse as the record.

06One pull, four different things

Now the part that pays for the setup. Everything below reads one file, and only the first script ever calls Google.

The pull

One dependency, Google's own Node library, whose default constructor reads the credential from GOOGLE_APPLICATION_CREDENTIALS: [21]

npm install @google-analytics/data

export GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json
export GA4_PROPERTY_ID=284916037
node fetch.mjs

Two environment variables and one command. That writes data.json beside the script, and everything below reads it. Here is fetch.mjs in full.

// Auth, one request, all arithmetic, one file out.
import { BetaAnalyticsDataClient } from '@google-analytics/data';
import { writeFileSync } from 'node:fs';

const PROPERTY = process.env.GA4_PROPERTY_ID;
const client = new BetaAnalyticsDataClient();

const [report] = await client.runReport({
  property: `properties/${PROPERTY}`,
  dateRanges: [
    { startDate: '2026-07-21', endDate: '2026-07-27' },
    { startDate: '2026-07-14', endDate: '2026-07-20' },
  ],
  dimensions: [{ name: 'sessionDefaultChannelGroup' }],
  metrics: [{ name: 'engagedSessions' }],
  orderBys: [{ metric: { metricName: 'engagedSessions' }, desc: true }],
  limit: 5,
});

// Two date ranges means the API appends a dateRange dimension, so both
// windows arrive in one list and get split apart by that last value.
const bucket = (key) => Object.fromEntries((report.rows ?? [])
  .filter((r) => r.dimensionValues.at(-1).value === key)
  .map((r) => [r.dimensionValues[0].value, Number(r.metricValues[0].value)]));

const now = bucket('date_range_0'), prev = bucket('date_range_1');

// Arithmetic lives here, in code, where it is testable and identical every run.
const findings = Object.keys(now).map((name) => {
  const a = now[name], b = prev[name] ?? 0;
  return { name, now: a, prev: b, abs: a - b,
           pct: b ? Number((((a - b) / b) * 100).toFixed(1)) : null };
});

const m = report.metadata ?? {};
writeFileSync('data.json', JSON.stringify({
  builtAt: new Date().toISOString(),
  property: PROPERTY,
  // Kept, not stripped. This is what keeps every reader below honest.
  quality: {
    sampled: (m.samplingMetadatas ?? []).length > 0,
    thresholded: Boolean(m.subjectToThresholding),
    truncated: Boolean(m.dataLossFromOtherRow),
    currencyCode: m.currencyCode ?? null,
    timeZone: m.timeZone ?? null,
  },
  findings,
}, null, 2));

Sixty lines, and everything after this reads its output. Note what it does with the metadata: it keeps it, in a quality object that travels with the numbers.

Output one: the dashboard

A single self-contained HTML file. No build step, no dependencies, no login. It opens instantly, works on a phone with bad signal, prints to a clean PDF, and nobody can change it under you. The prompt that produces one is long on purpose, because every line exists to prevent something worse.

Read data.json and write ONE self-contained HTML file.

No external requests of any kind: no CDN scripts, no web fonts, no
remote images, no fetch calls. Inline all CSS. Charts are
hand-written inline SVG, not a charting library.

Layout: a header with the property, the date range and the build
timestamp; the figures as tiles with a percentage change each; the
main table with a share bar per row; a one-line status strip along
the bottom.

Every number comes from data.json. If a field is missing, render
"no data", never a zero. A zero and a missing value are different
things and must never look the same.

Format currency with quality.currencyCode and times with
quality.timeZone. Do not assume dollars and do not assume UTC.

The status strip reports the sampling rate, whether thresholding
was applied, whether an (other) row is present, and how many
requests built the page.

Any table where quality.truncated is true is labeled a top-N,
never a total, and shows the (other) row rather than dropping it.

Inline SVG rather than a library is not aesthetic preference. A sparkline is a polyline with eight points, so the library was never buying much, and it can cost you the chart outright: many sites run a content security policy, the browser rule deciding which scripts may execute, and a strict one blocks the runtime code generation some charting libraries need. [17] You get a container and no chart, with nothing in the page explaining why.

Output two: the reports the interface will not draw

Two report types take a single request and are either unavailable or miserable in the Analytics interface.

Cohort retention. Add a cohortSpec to a normal runReport and Google builds "a time series of user retention for the cohort." [4]

"cohortSpec": {
  "cohorts": [
    {
      "dimension": "firstSessionDate",
      "dateRange": { "startDate": "2020-12-01", "endDate": "2020-12-01" }
    }
  ],
  "cohortsRange": { "endOffset": 5, "granularity": "DAILY" }
}

Switch granularity to WEEKLY, widen endOffset, and you get the grid that tells you whether a product change actually stuck.

A cohort retention grid of seven weekly cohorts against eight week columns, cells shaded from pale to solid brand blue by retention rate, with the June 15 cohort labeled onboarding shipped standing out at 52.8 percent in week one
Read it diagonally, not across. Every cohort from June 15 holds where the earlier ones fell away. Sample data.Dashboard: Mediovsky · rendered from sample data

That shape answers a question a line chart cannot: not "did retention improve" but "did it improve for people who arrived after the change, and did it hold." Weekly totals would have shown a mild lift and buried the cause.

Funnels use runFunnelReport, with steps defined by filter expressions, and there is a real catch. Google is explicit: "Funnel reporting feature is only available in the v1alpha version of the Google Analytics Data API. While we will try to notify you of upcoming changes, you should expect to encounter breaking changes before this feature is publicly released." [5] Use it in conversation, keep it out of the scheduled job.

A five-step signup funnel with descending brand-blue bars and drop-off percentages between them, a highlighted 62.1 percent card on the right, and an amber pill warning that funnel reporting is v1alpha only
Four of the five steps are fine. Printing the drop between them is what makes that obvious in one look. Sample data.Dashboard: Mediovsky · rendered from sample data

Output three: the written weekly summary

The dashboard serves people who open dashboards. Most stakeholders do not. What they read is an email, and this is where a language model earns its place, because turning eleven computed facts into four readable sentences is exactly what it is good at.

The rule that keeps it safe: the model receives findings and never the raw rows, so it has nothing to compute with.

You are writing the analytics section of a Monday update. You are
given a JSON object with computed findings. Every number in it is
already correct. Do not recalculate anything, and do not introduce
a figure that is not in the input.

Write at most five sentences. Lead with the single largest mover in
absolute terms, not percentage terms. Group anything smaller into
one closing sentence.

If quality.thresholded or quality.truncated is true, open with one
short sentence saying which parts of the picture are incomplete.

Do not explain why anything changed. You do not have deploy logs,
campaign calendars, or seasonality. If a cause seems obvious, say
what you would need in order to check it.

That last paragraph is the whole discipline. Analytics does not know a deploy shipped on Wednesday, so any "because" in the draft came from the model, not the data. Forbidding causal language costs nothing and removes the single most common way these summaries become confidently wrong.

Output four: the alert that stays quiet

An alert is the opposite of a report. Its job is to send nothing almost every week. alert.mjs reads the same file and posts to a Slack incoming webhook, which takes a JSON body as simple as {"text": "Hello, world."} at a URL of the form https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXX. [22]

import { readFileSync } from 'node:fs';
const { findings, quality } = JSON.parse(readFileSync('data.json', 'utf8'));

// A truncated report can fake a collapse, so it never raises an alarm.
if (quality.truncated) process.exit(0);

// Two conditions, not one: a big percentage on a small number is noise.
const breached = findings.filter((f) =>
  f.pct !== null && Math.abs(f.pct) >= 15 && Math.abs(f.abs) >= 500);
if (!breached.length) process.exit(0);   // silence is the default

const lines = breached.map((f) =>
  `${f.name}: ${f.pct > 0 ? '+' : ''}${f.pct}%  (${f.now} vs ${f.prev})`).join('\n');

await fetch(process.env.SLACK_WEBHOOK_URL, {
  method: 'POST',
  headers: { 'Content-type': 'application/json' },
  body: JSON.stringify({ text: `*Engaged sessions moved*\n${lines}` }),
});

Two thresholds rather than one is the design decision that matters. A 40% rise on 12 sessions is noise. A 6% fall on 40,000 sessions is the only thing worth waking up for. Requiring both a relative and an absolute move stops small denominators generating drama.

The same file can reach anywhere that accepts an HTTP request. Email through a transactional provider, SMS or push through a messaging one, a row appended to a sheet. Each is another reader of data.json, which is why adding one does not touch quota, credentials, or the pull.

The shape worth remembering

Pull once, compute in code, then render or deliver, as many times as you like.

One request, four destinations, one set of numbers
Free · one-page PDF

The GA4 dashboard prompt pack

Every prompt in this piece on one page, in running order, plus the review pass that catches a page implying more precision than its data supports.

  • Pull the data, keeping the response metadata instead of stripping it
  • Build one self-contained page with hand-written SVG and no external requests
  • Write the weekly summary without letting the model compute or speculate
Get the prompt pack →

07Letting it run without letting it lie

Three layers, and only one of them gets to be creative. The script pulls and writes. Code computes every comparison, because arithmetic has a correct answer and belongs somewhere testable. Only then does a model turn surviving facts into prose.

A plaster hand lowering a small dark weight onto one pan of a brass balance scale while the opposite pan holds a loose curl of paper, a solid brand-blue disk behind the scale and cropped classical heads at the edges
One side gets weighed. The other only gets written. Keeping them on separate pans is the entire design.Illustration: Mediovsky · generated with AI

Then schedule against the data rather than the calendar. Google states that "Data processing can take 24-48 hours. During that time, data in your reports may change," with standard properties typically seeing intraday data in 2 to 6 hours and 360 properties seeing it in about an hour. [11] A Monday 6 a.m. job reporting on Sunday sits inside that window. If you pull Search Console alongside, it adds its own caveat: "The newest data can be preliminary, meaning it's still being collected and might change in the next few hours." [18] It also meters separately, at 1,200 queries per minute per site, and warns that grouping or filtering by page or query is the expensive shape. [19]

The fix is unglamorous: end the reporting window a day or two before the run, and print on every output which window it covers.

Scheduling itself is one line. On a server, a crontab entry. On a hosted runner, the same expression in its own scheduler:

# m h dom mon dow   command
  0 7  *   *   2     cd /srv/report && node fetch.mjs && node alert.mjs

Tuesday rather than Monday, and seven rather than six, for the reason above: it puts the whole reporting window outside the processing lag instead of racing it.

The failure to design for

A report that silently arrives incomplete is worse than one that does not arrive.

Almost every early break is plumbing: an expired credential, a property whose access was granted by someone who left, a quota error swallowed by a retry, a report that quietly changed shape. Make each fail loudly and stop the send. A missing email prompts a question. A thinner-than-usual email prompts a decision, made on numbers nobody knows are partial.

Keep the human gate on anything a client reads. It costs a minute and catches the week when a tracking change made every number look catastrophic. The dashboard and the alert can run unattended, because the worst case is a page nobody opened and a message nobody needed.

  • Grant the service account Viewer on the property, not just a valid credential. [20]
  • Name dimensions and metrics by their API names, so the assistant stops guessing. [12]
  • Put both periods in one dateRanges array rather than making two calls. [3]
  • Keep the metadata beside the numbers, and render what it says. [6]
  • Give the model finished figures and forbid causal language.
  • Require two thresholds before an alert fires, one relative and one absolute.
  • Keep funnels out of unattended jobs while the endpoint is alpha. [5]
  • Send figures that must be complete to the export, and mind its 1-million-event ceiling. [16]

Two years ago wiring a model into an analytics property was a project. Now it is an afternoon, and the setup is the least interesting thing in this piece. What is left is the part that was always hard: choosing which question is worth answering every week, and knowing whether the answer you got is the one you think it is.

Sources

  1. Google · Google Analytics MCP Server (Experimental)the seven tools, the two APIs to enable, the analytics.readonly scope, the claude mcp add and gcloud commands, and the GOOGLE_CLOUD_PROJECT versus GOOGLE_PROJECT_ID mismatch
  2. Google · Analytics Data API v1 quotas200,000 daily and 40,000 hourly core tokens, 14,000 per project per property per hour, 10 concurrent requests; 2,000,000 and 50 on 360
  3. Google · Analytics Data API v1 basicsthe runReport endpoint and the dateRanges, dimensions, and metrics request body
  4. Google · Advanced use casescohortSpec structure, and that cohort reports create a time series of user retention for the cohort
  5. Google · Funnel reportsrunFunnelReport, and the warning that funnel reporting is v1alpha only with breaking changes expected
  6. Google · Analytics Data API ResponseMetaDatasubjectToThresholding, dataLossFromOtherRow, samplingMetadatas with samplesReadCount and samplingSpaceSize, and emptyReason
  7. Anthropic · Connect Claude Code to tools via MCPclaude mcp add, the transport flags, and local, project, and user scopes
  8. Microsoft · Use MCP servers in VS Codenative support, the servers top-level key in .vscode/mcp.json, and the MCP: Add Server command
  9. Cursor · Model Context Protocol.cursor/mcp.json with the mcpServers key, and the stdio, SSE, and streamable HTTP transports
  10. Google · Method: properties.runReporta maximum of 250,000 rows per request, and 10,000 returned when limit is unspecified
  11. Google · Data freshness for Google Analytics"Data processing can take 24-48 hours"; 2-6 hours intraday on standard properties, about 1 hour intraday on 360
  12. Google · API dimensions and metricsthe API-name vocabulary, including sessionDefaultChannelGroup, engagedSessions, firstSessionDate, eventName, pagePath, and deviceCategory
  13. Google · Data thresholdsthe purpose of thresholding, and that thresholds are system defined and cannot be adjusted
  14. Google · About data samplingthe 10 million event-level query limit on standard properties, 1 billion on 360 from a 100 million default
  15. Google · About the (other) rowthe definition of the (other) row, the 500-value high-cardinality guidance, and the advice against per-user custom dimensions
  16. Google · BigQuery Export for Google Analytics 41 million events per day on the standard daily export, paused without reprocessing when exceeded
  17. MDN · Content-Security-Policy: script-srchow a policy without unsafe-eval blocks runtime code generation, which is what silently kills library-drawn charts
  18. Google · Search Console Performance reportthe newest data can be preliminary and might change in the next few hours
  19. Google · Search Console API usage limits1,200 queries per minute per site, and the warning that grouping or filtering by page or query is expensive
  20. Google · Analytics roles and data restrictionsProperty access management, and the Viewer role that can see settings and data
  21. Google · Data API quickstart with client librariesthe @google-analytics/data package, BetaAnalyticsDataClient, and the default constructor reading GOOGLE_APPLICATION_CREDENTIALS
  22. Slack · Sending messages using incoming webhooksthe webhook URL format and the JSON body shape a POST must carry

Frequently asked questions

What do I need before connecting GA4 to an AI assistant?

A Google Cloud project with the Analytics Admin API and the Analytics Data API enabled, a credential, and access granted inside Google Analytics itself. That last step is the one people miss: a service account is not a Google Analytics user until you add its email address under Admin, Property access management, with at least the Viewer role.

Is there an official Google Analytics MCP server?

Yes. Google publishes it at github.com/googleanalytics/google-analytics-mcp and titles it Google Analytics MCP Server (Experimental). It runs locally through pipx and exposes seven tools, all of them reads: get_account_summaries, get_property_details, list_google_ads_links, run_report, run_funnel_report, get_custom_dimensions_and_metrics, and run_realtime_report.

Can an MCP server change my Analytics configuration?

Google's cannot. Its README requires credentials carrying the read-only Analytics scope, so the permission forbids writes rather than the tool descriptions merely promising it. On any community server, read the scope on the consent screen instead of the claim in the README.

Should I use MCP or the Data API?

Both, for different jobs. MCP is a conversation, so it suits open questions while a human is present to catch a wrong turn. The Data API in code suits anything scheduled, because a cron job has nobody to approve a tool call. The working sequence is to explore in MCP until you know what the report should say, then move that settled request into a file.

What does the fetch script actually do?

It authenticates with the service account, sends one runReport call, splits the two date ranges apart, computes every change in plain JavaScript, and writes data.json with the response metadata kept alongside the numbers. It is the only file that talks to Google. The dashboard, the email, and the alert all read that file and make no API calls of their own.

Can it do more than dashboards?

That is the point of splitting the pull from the render. The same data.json feeds an HTML dashboard, a written weekly summary sent by email, and a Slack alert that fires only when a change clears a threshold. Adding a channel means adding a reader, not another API call, so the quota cost does not change.

How do I stop the model from inventing numbers?

Never let it compute one. The fetch script does the arithmetic in code, where it is testable and identical every run, and the model receives finished figures and writes sentences about them. Forbid causal language too, because Analytics does not know that a deploy shipped on Wednesday, so any because in the draft came from the model.

How do I know when a GA4 number is incomplete?

Read the ResponseMetaData block that comes with every report. subjectToThresholding means privacy suppression removed rows, dataLossFromOtherRow means values were rolled into an (other) row, and samplingMetadatas gives samplesReadCount over samplingSpaceSize so you can compute the sampling rate. None of them raise an error.

What are the Data API quota limits?

A standard property gets 200,000 core tokens per day and 40,000 per hour, with 10 concurrent requests, and a single Cloud project is separately capped at 14,000 core tokens per property per hour. Analytics 360 raises every token ceiling tenfold and lifts concurrency to 50. Tokens are charged by query complexity, not request count.

When should the scheduled job run?

Later than feels natural. Google says data processing can take 24 to 48 hours and that reports may change during that window, with standard properties typically seeing intraday data in 2 to 6 hours. End the reporting window a day or two before the run, and print on the output which window it covers.

Do funnels work through the API?

Yes, through runFunnelReport, but Google warns that funnel reporting is only available in the v1alpha version and that you should expect breaking changes before it is publicly released. Use it interactively, and keep it out of anything scheduled and unattended until it settles.

Found this useful?
MSMikołaj Salecki, portrait
Editor-in-chief

Mikołaj Salecki

Writes about media, tech, and AI business for people who actually run digital. Former agency lead. Skeptic of frameworks that read better than they perform.

More articles →