Simon Breton Simon Breton - 3 months ago 19
Javascript Question

Lost in callback hell with Node, Express and google Analytics API

I'm a beginner here so I'll try to expose my issue the best way I can. I'm trying to call the Google Analytics API on the server-side to show some data on the client-side with react and d3. Here is my script to call the GA api :

var google = require ("googleapis");
var key = require ('./client_id.json');

const VIEW_ID = 'ga:80820965';

let jwtClient = new google.auth.JWT(
key.client_email,
null,
key.private_key,
['https://www.googleapis.com/auth/analytics.readonly'],
null
);

jwtClient.authorize(function (err, tokens) {
if (err) {
console.log(err);
return;
}
let analytics = google.analytics('v3');
queryData(analytics);
});

var queryData = function(analytics) {
analytics.data.ga.get({
'auth': jwtClient,
'ids': VIEW_ID,
'metrics': 'ga:uniquePageviews',
'dimensions': 'ga:pagePath',
'start-date': '30daysAgo',
'end-date': 'yesterday',
'sort': '-ga:uniquePageviews',
'max-results': 10,
}, function (err, response) {
if (err) {
console.log(err);
return;
}
console.log(JSON.stringify(response, null, 4));
});
}


module.exports = {
queryData
};


When I calling this script independently everything is fine, however when I'm calling it with my whole project I've got the following error :

[1] analytics.data.ga.get({
[1] ^
[1]
[1] TypeError: Cannot read property 'data' of undefined


From what I have understand
data
is undefined because
analytics
is undefined. And
analytics
is undefined because this call :

jwtClient.authorize(function (err, tokens) {
if (err) {
console.log(err);
return;
}
let analytics = google.analytics('v3');
data = queryData(analytics);
});


is an async call. I guess the time the client_id is loaded.

So how can I write this script so that
queryData
is running only when this part :

let analytics = google.analytics('v3');
data = queryData(analytics);


is ready ?

I hope my issue is clear and make sense to you. I've read about async/await module. However I'm not enable to use it my self.

Edit #1 : I need to use something like this but I'm not sure how

module.exports = {queryData: function (err, req, res, next) { next(); }};

Answer

You're defining analytics as a local variable here:

jwtClient.authorize(function (err, tokens) {
  if (err) {
    console.log(err);
    return;
  }
  let analytics = google.analytics('v3');
    queryData(analytics);
});

And then expecting it to be available here in queryData(analytics) in your export.

What you can do instead is resolve your authorize call in a promise (since there will be a race condition when it first initializes, so you can wrap it in a promise and wait for it to resolve before being able to call queryData). Something like this:

var google = require ("googleapis");
var key = require ('./client_id.json');
const Promise = require('bluebird');
var authorizationPromise;

const VIEW_ID = 'ga:80820965';

let jwtClient = new google.auth.JWT(
  key.client_email, 
  null,
  key.private_key,
  ['https://www.googleapis.com/auth/analytics.readonly'],
  null
);

authorizationPromise = Promise.promisify(jwtClient.authorize)()
.then(function (err, tokens) {
  if (err) {
    throw new Error(err);
  }

  return google.analytics('v3');
})
.catch(function(err) {
  console.log(err);
});

var queryData = function() {
  authorizationPromise.then(function(analytics) {
      analytics.data.ga.get({
        'auth': jwtClient,
        'ids': VIEW_ID,
        'metrics': 'ga:uniquePageviews',
        'dimensions': 'ga:pagePath',
        'start-date': '30daysAgo',
        'end-date': 'yesterday',
        'sort': '-ga:uniquePageviews',
        'max-results': 10,
      }, function (err, response) {
        if (err) {
          console.log(err);
          return;
        }
        console.log(JSON.stringify(response, null, 4));
      }); 
  });
};

module.exports = {
    queryData
};

I'm not sure off-hand whether jwtClient.authorize provides a promise API, so I just "promisified" it using bluebird.