Interact with the Google Analytics API using Node.js
By Flavio Copes
Learn how to query the Google Analytics Data API (GA4) from Node.js with a service account, using runReport for sessions and traffic data.
In this post I’m going to show some examples of using the Google Analytics Data API (GA4) with Node.js.
This post originally used Universal Analytics (analytics('v3'), data.ga.get(), view IDs like ga:XXXX). Universal Analytics stopped processing data on July 1, 2023, and its API was switched off on July 1, 2024, so that code doesn’t work anymore. Everything below uses a GA4 property and the Data API.
- Environment variables
- Add the user to Google Analytics
- Import the Google library
- Define the scope
- Create the JWT
- Perform a request
- Metrics and dimensions
- Common code
- Get today’s sessions
- Get organic sessions today
- Get yesterday’s sessions
- Get sessions in the last 30 days
- Get browsers used in the last 30 days
- Get Chrome sessions
- Get sessions by traffic source
- Realtime reports
There are two npm packages you can use. The big googleapis package covers every Google API, including the Data API through google.analyticsdata('v1beta'). The smaller @google-analytics/data package only does Analytics. I checked the code with googleapis 181 (the package bumps its major version very often, so don’t worry if you get a higher number) and @google-analytics/data 7:
npm install googleapis
# or:
npm install @google-analytics/data@7
Most examples use googleapis, because authentication works the same way as in my post on how to authenticate to the Google APIs. I’m going to assume you read that, and you know how to perform a JWT authentication with a service account.
Environment variables
Once you download the JSON key file from Google, put the client_email and private_key values as environment variables, so that they will be accessible through
process.env.CLIENT_EMAILprocess.env.PRIVATE_KEY
Also set your GA4 property ID (digits only, from Admin > Property settings), e.g. process.env.GA4_PROPERTY_ID.
Add the user to Google Analytics
Since we’re using the Service to Service API in these examples, you need to add the client_email value to your GA4 property. In Admin, open Property access management, click the + button and choose Add users.
Enter the email you found in the client_email key in the JSON file, and give it the Viewer role.
Import the Google library
const { google } = require('googleapis')
Remember the {} around the google object, as we need to destructure it from the googleapis library (otherwise we’d need to call google.google and it’s ugly).
With @google-analytics/data you import BetaAnalyticsDataClient instead. You’ll see an example of it below.
Define the scope
This line sets the scope:
const scopes = 'https://www.googleapis.com/auth/analytics.readonly'
You should always pick the scope that grants the least amount of power. For read-only reports, analytics.readonly is enough.
Create the JWT
const jwt = new google.auth.JWT({
email: process.env.CLIENT_EMAIL,
key: process.env.PRIVATE_KEY,
scopes
})
Older examples (including the first version of this post) passed the email, the key and the scopes as separate arguments, with a null in between for the key file path. google-auth-library 10 removed that form. If you still use it, the JWT gets created with no email and no key, and every request fails with an authentication error. Pass an object instead.
Perform a request
Check this code:
const { google } = require('googleapis')
const scopes = 'https://www.googleapis.com/auth/analytics.readonly'
const jwt = new google.auth.JWT({
email: process.env.CLIENT_EMAIL,
key: process.env.PRIVATE_KEY,
scopes
})
const propertyId = process.env.GA4_PROPERTY_ID // e.g. '123456789'
async function getData() {
await jwt.authorize()
const analyticsdata = google.analyticsdata('v1beta')
const result = await analyticsdata.properties.runReport({
auth: jwt,
property: `properties/${propertyId}`,
requestBody: {
dateRanges: [{ startDate: '30daysAgo', endDate: 'today' }],
metrics: [{ name: 'screenPageViews' }]
}
})
console.dir(result.data)
}
getData()
It performs a request to the GA4 Data API to fetch the page views in the last 30 days (GA4 calls the metric screenPageViews, because it counts app screens too).
propertyId is the GA4 property ID, a number like 123456789. It’s not the Measurement ID that starts with G-, and not an old Universal Analytics view ID.
result.data looks like this:
{
metricHeaders: [{ name: 'screenPageViews', type: 'TYPE_INTEGER' }],
rows: [{ dimensionValues: [], metricValues: [{ value: '114426' }] }],
rowCount: 1,
metadata: { currencyCode: 'USD', timeZone: 'Europe/Rome' },
kind: 'analyticsData#runReport'
}
Every row has a dimensionValues array and a metricValues array, in the same order as you asked for them. So the page views count is in result.data.rows[0].metricValues[0].value, and it’s a string.
Same idea with @google-analytics/data. Here the credentials go straight into the client constructor, and runReport() takes the request object directly. It returns an array whose first element is the response:
const { BetaAnalyticsDataClient } = require('@google-analytics/data')
const client = new BetaAnalyticsDataClient({
credentials: {
client_email: process.env.CLIENT_EMAIL,
private_key: process.env.PRIVATE_KEY
}
})
async function getData() {
const [response] = await client.runReport({
property: `properties/${process.env.GA4_PROPERTY_ID}`,
dateRanges: [{ startDate: '30daysAgo', endDate: 'today' }],
metrics: [{ name: 'screenPageViews' }]
})
console.dir(response.rows)
}
getData()
Metrics and dimensions
Dimensions are attributes, like City, Country or Page.
Metrics are quantitative measurements, like active users or sessions.
Some useful GA4 metric names:
screenPageViewsactiveUserssessionsorganicGoogleSearchClicks, which only works if you linked Search Console to the property. Otherwise filter sessions by medium, like in the example below
Browse the current list in the GA4 Dimensions & Metrics Explorer.
Common code
Here is the common code used in the examples below:
'use strict'
const { google } = require('googleapis')
const scopes = 'https://www.googleapis.com/auth/analytics.readonly'
const jwt = new google.auth.JWT({
email: process.env.CLIENT_EMAIL,
key: process.env.PRIVATE_KEY,
scopes
})
const analyticsdata = google.analyticsdata('v1beta')
const property = `properties/${process.env.GA4_PROPERTY_ID}`
async function getData() {
await jwt.authorize()
/* custom code goes here */
}
getData()
Get today’s sessions
const result = await analyticsdata.properties.runReport({
auth: jwt,
property,
requestBody: {
dateRanges: [{ startDate: 'today', endDate: 'today' }],
metrics: [{ name: 'sessions' }]
}
})
console.dir(result.data.rows?.[0]?.metricValues?.[0]?.value)
Get organic sessions today
Filter on the session medium dimension:
const result = await analyticsdata.properties.runReport({
auth: jwt,
property,
requestBody: {
dateRanges: [{ startDate: 'today', endDate: 'today' }],
metrics: [{ name: 'sessions' }],
dimensionFilter: {
filter: {
fieldName: 'sessionMedium',
stringFilter: { value: 'organic' }
}
}
}
})
Get yesterday’s sessions
const result = await analyticsdata.properties.runReport({
auth: jwt,
property,
requestBody: {
dateRanges: [{ startDate: 'yesterday', endDate: 'yesterday' }],
metrics: [{ name: 'sessions' }]
}
})
console.dir(result.data.rows?.[0]?.metricValues?.[0]?.value)
Get sessions in the last 30 days
const result = await analyticsdata.properties.runReport({
auth: jwt,
property,
requestBody: {
dateRanges: [{ startDate: '30daysAgo', endDate: 'today' }],
metrics: [{ name: 'sessions' }]
}
})
console.dir(result.data.rows?.[0]?.metricValues?.[0]?.value)
Get browsers used in the last 30 days
const result = await analyticsdata.properties.runReport({
auth: jwt,
property,
requestBody: {
dateRanges: [{ startDate: '30daysAgo', endDate: 'today' }],
dimensions: [{ name: 'browser' }],
metrics: [{ name: 'sessions' }]
}
})
console.dir(
(result.data.rows || []).sort(
(a, b) => Number(b.metricValues[0].value) - Number(a.metricValues[0].value)
)
)
Get Chrome sessions
const result = await analyticsdata.properties.runReport({
auth: jwt,
property,
requestBody: {
dateRanges: [{ startDate: '30daysAgo', endDate: 'today' }],
dimensions: [{ name: 'browser' }],
metrics: [{ name: 'sessions' }],
dimensionFilter: {
filter: {
fieldName: 'browser',
stringFilter: { value: 'Chrome' }
}
}
}
})
console.dir(result.data.rows?.[0]?.metricValues?.[0]?.value)
Get sessions by traffic source
const result = await analyticsdata.properties.runReport({
auth: jwt,
property,
requestBody: {
dateRanges: [{ startDate: '30daysAgo', endDate: 'today' }],
dimensions: [{ name: 'sessionSource' }],
metrics: [{ name: 'sessions' }]
}
})
console.dir(
(result.data.rows || []).sort(
(a, b) => Number(b.metricValues[0].value) - Number(a.metricValues[0].value)
)
)
Realtime reports
When I first wrote this post the Universal Analytics realtime API was a private beta. On GA4 realtime data is part of the same Data API: call analyticsdata.properties.runRealtimeReport() with the same auth and property, and a requestBody with metrics (for example activeUsers) and optional dimensions. There is no date range, since it only covers the last 30 minutes. The Data API docs list the realtime dimensions and metrics, which are a smaller set than the report ones.
Want me to talk about your product? You can sponsor this site.
Related posts about node: