Using the Google Analytics Management API with Node.js

Written by Tom Wilkins
Sun, 13 Oct 2019

Automate your Google Analytics configuration using this API with Node.js

Using the Google Analytics Management API with Node.js

The Google Analytics Management API allows developers to read and edit various configuration options for Google Analytics programatically.

The options include:

  1. Custom Dimensions
  2. Custom Metrics
  3. Goals

Why might you want to manage your configuration this way?

Managing this configuration for one property in analytics is easy enough in the interface. If you have more than one property though, where you want to share all, or some of your custom configurations, it becomes a large manual effort to syndicate that out which is of course prone to mistakes.

I recently faced this challenge, and having used the Analytics Reporting API for Node.js I thought it would be worth exploring the Management API. Both are available in the googleapis client library for Node.js.

At the time of writing the Management API library for Node.js is in alpha. Because of this, I've found documentation to be limited which has been my main motivation for this post. What I've cheekily done is read the docs for the Python client library and adapted it to Node.

Getting Set Up

To use this you'll need:

  1. Node.js and NPM installed
  2. Google Analytics Account
  3. Google Service Account Credentials

If you are new to analytics and/or Node I would strongly suggest creating a test account in Analytics to work with. Once you're comfortable with peforming the operations you need, you can then use them on a production account.

Authentication

All requests need to be authenticated to ensure that you have sufficient permissions to read/write the account in question. As we're building a command line tool, service to service authentication makes the most sense. You can create credentials by logging into the Google Cloud Console and generating a Service Account Key - this will give you a JSON file that you can save in your working directory.

Setting up your Analytics Management module to be authenticated can look like this:

google-analytics-management.js

const { google } = require("googleapis");
const key = require("./service-account-auth.json");
const scopes = [
"https://www.googleapis.com/auth/analytics",
"https://www.googleapis.com/auth/analytics.edit"
];
const jwt = new google.auth.JWT(
key.client_email,
null,
key.private_key,
scopes
);
const analytics = google.analytics({
version: "v3",
auth: jwt
});
module.exports = {
// analytics methods
};

Read Methods

The way I have decided to manage this process is by defining my configuration using the web UI for one account, and then using that as the template to distribute the configuration across more properties.

There are two methods from which to read properties, get and list. The get method will retrieve details for one particular custom property, while list will retrieve all custom properties for the account.

I'll begin constructing my module like this:

google-analytics-management.js

// authentication
module.exports = {
checkStatus: function(res) {
return new Promise(function(resolve, reject) {
if (res.status === 200) {
resolve(res.data);
} else {
reject(res.status);
}
});
},
getPropertyLabel: function(dimensionMetric) {
return "custom" + dimensionMetric + "s";
},
getCustomProperty: async function(dimensionMetric, account, index) {
const { accountId, webPropertyId } = account;
const property = this.getPropertyLabel(dimensionMetric);
const res = await analytics.management[property].get({
accountId,
webPropertyId,
[`custom${dimensionMetric}Id`]: `ga:${dimensionMetric +
index}
`
.toLowerCase()
});
return this.checkStatus(res);
},
listCustomProperties: async function(dimensionMetric, account) {
const { accountId, webPropertyId } = account;
const property = this.getPropertyLabel(dimensionMetric);
const res = await analytics.management[property].list({
accountId,
webPropertyId
});
return this.checkStatus(res);
},
getGoal: async function(account, goalId) {
const { accountId, webPropertyId, profileId } = account;
const res = await analytics.management.goals.get({
accountId,
webPropertyId,
profileId,
goalId
});
return this.checkStatus(res);
},
listGoals: async function(account) {
const { accountId, webPropertyId, profileId } = account;
const res = await analytics.management.goals.list({
accountId,
webPropertyId,
profileId
});
return this.checkStatus(res);
}
};

There are a few things going on here so I'll explain each method:

checkStatus

All the methods we call from this library boil down to http requests. Each request will return a full http response, and we're only interested in two things - the status code, and the data we have asked for. If the status code is 200, we'll resolve a promise with the data, otherwise we'll reject it with the status code.

This is common code all methods will rely upon, so it makes sense to break this out into a separate method.

getPropertyLabel

The methods for custom dimensions and custom metrics are so similar that I've decided to write one method using parameters to determine which to retrieve. This means that I can invoke getCustomProperty and pass it an argument of 'Dimension' or 'Metric'. All getPropertyLabel does is take that argument and return the correct property name to use in the method call.

getCustomProperty

This uses the logic explained above to retrieve either a custom dimension or custom metric. We have the index as a parameter to know which custom property to retrieve, and the account object.

From the account object we use destructuring to get the accountId and webPropertyId which form part of our request. The last property in our request looks a little complicated - I'm just using template string literals to construct a property that will either be 'customDimensionId' or 'customMetricId' depending on the dimensionMetric parameter, with a similar thing for the value of this parameter.

listCustomProperties

This works in a similar way to getCustomProperty, but we don't need an index argument. We're simply listing all custom dimensions or metrics for the property specified in our account object.

getGoal

Very much like getting a custom dimension or metric, however Goals are defined at view (or profile) level - so we need an additional property in our request to define that, as well as the id for the goal we are trying to retrieve.

listGoals

Similar to getGoal but as we are listing them all, we don't need to specify a goal id in the request.

Using these methods

Since each method runs though checkStatus, they all return a promise. When using them we can either await their results inside an async function, or chain them with a .then().

I'll come onto working with the data that comes back in the next section.

Write Methods

Let's expand our module with some write methods:

module.exports = {
// read methods as above, then...
editCustomProperty: async function(
action,
dimensionMetric,
account,
requestBody

) {
const { accountId, webPropertyId } = account;
const property = this.getPropertyLabel(dimensionMetric);
const res = await analytics.management[property][action]({
accountId,
webPropertyId,
requestBody
});
return this.checkStatus(res);
},
editGoal: async function(action, account, requestBody) {
const { accountId, webPropertyId, profileId } = account;
const res = await analytics.management.goals[action]({
accountId,
webPropertyId,
profileId,
requestBody
});
return this.checkStatus(res);
},
copyConfiguration: function(config) {
const result = {};
const excludeKeys = [
"accountId",
"webPropertyId",
"profileId",
"parentLink",
"selfLink",
"internalWebPropertyId",
"created",
"updated"
];
for (let key in config) {
if (!excludeKeys.includes(key)) result[key] = config[key];
}
return result;
}
};

editCustomProperty

Again, I'm writing a common method that will accept 'Dimension' or 'Metric' as a parameter to determine which type of custom property to edit. I've also added a parameter of 'action'. This will determine whether we 'insert' a new custom property, or 'update' an existing one.

editGoal

As above but with the inclusion of the profileId, as goals are defined at a profile level.

requestBody

If we are adding or updating configuration, we need to define that configuration here.

copyConfiguration

I've written this as a helper function. When copying config, there will be different properties based on the configuration for the custom property. For instance, a time on site goal will need to define a duration, and a destination goal will define a url.

This helper function will copy the configuration we get back from the API, excluding things like the webPropertyId from the account we are copying from. This is so that when we make a request to add that config elsewhere, we aren't providing conflicting account information.

Bringing it all together

So now we have both sides of the coin. We can use the read methods to retrieve configuration from one account, and the write methods to copy that configuration to other accounts. We can define all of that in a separate script and import this module to help.

Bear with me, this is a rather long script but I'll explain each part of it. You may need to scroll the pane to see all of the comments:

copy-ga-config.js

(async function() {
// creating an async IIFE to make use of async await
const analytics = require("./google-analytics-management.js");
const templateAccount = {
// the account to copy settings from
accountId: "123456789",
webPropertyId: "UA-XXXXXXXX-1",
profileId: "123456789"
};
const childAccounts = [
// the accounts to copy settings to
{
accountId: "123456789",
webPropertyId: "UA-YYYYYYYYY-1",
profileId: "123456789"
},
{
accountId: "123456789",
webPropertyId: "UA-ZZZZZZZZZ-1",
profileId: "123456789"
}
];
// retrieving goals, dimensions and metrics from the template account
const { items: goals } = await analytics.listGoals(templateAccount);
const { items: Dimension } = await analytics.listCustomProperties(
"Dimension",
templateAccount
);
const { items: Metric } = await analytics.listCustomProperties(
"Metric",
templateAccount
);
/*
putting dimensions and metrics into an object we can iterate through
because the logic to update them can be shared
*/

const customProperties = {
Dimension,
Metric
};
// iterating through the child accounts to copy to
for (let i = 0; i < childAccounts.length; i++) {
const account = childAccounts[i];
// getting current goals
const { items: currentGoals } = await analytics.listGoals(account);

// iterating through our template goals
for (let j = 0; j < goals.length; j++) {
// copying config, excluding template account info using the helper function
const requestBody = analytics.copyConfiguration(goals[j]);
// checking if a goal exists for this id and assigning the right action
const action = currentGoals.some(goal => goal.id === requestBody.id)
? "update"
: "insert";
// making the request
const res = await analytics.editGoal(
action,
account,
requestBody.id,
requestBody
);
// logging the result to the console
console.log(action, "goal", res.id, res.webPropertyId);
}

const propertyKeys = Object.keys(customProperties);
// making an array from our customProperties object to iterate through
for (let j = 0; j < propertyKeys.length; j++) {
const key = propertyKeys[j];
// retrieving the current custom dimensions/metrics for the child account
const { items: currentProps } = await analytics.listCustomProperties(
key,
account
);
// iterating through custom dimensions/metrics from the template
for (let k = 0; k < customProperties[key].length; k++) {
const customProp = customProperties[key][k];
// building the requestBody with the helper function
const requestBody = analytics.copyConfiguration(customProp);
// assigning the appropriate action
const action = currentProps.some(prop => prop.id === requestBody.id)
? "update"
: "insert";
// making the request
const res = await analytics.editCustomProperty(
action,
key,
account,
requestBody.index,
requestBody
);
// logging the result to the console
console.log(action, res.id, res.webPropertyId);
}
}
}
})();

Essentially, we are grabbing all goals, custom dimensions and custom metrics from our template account, and then for each child account we are:

  1. Retrieving all current goals/dimensions/metrics
  2. Checking to see if the action required is an 'update' or 'insert' based on what the child account currently has, for each goal/dimension/metric we want to copy over
  3. Building a request body from the template and either adding to, or updating the corresponding goal/dimension/metric in the child account

Simple right?

Last word

This is just one interesting use case that I have encountered. There are many more operations available through the Analytics Management API which you can read in the docs linked at the top of this post.

Google has many examples of API usage but again, at the time of writing this post those examples are limited to Java, Python, with some JavaScript examples relating to the client-side API. Hopefully this post helps show how the API can be used in a Node.js command line application.

Thank you for reading

You have reached the end of this blog post. If you have found it useful, feel free to share it on Twitter using the button below.

Tags: TIL, Node.js, JavaScript, Blog, Google Analytics