How to measure requests (XHR) duration in AngularJS?

1.1k Views Asked by At

I'm trying to implement an interceptor for AngularJS (1.5.x), which can measure each api calls and send the duration of each of them to the Google Analytics.

I've started with a dummy implementation of this, using just new Date().getTime():

(function (angular) {
  'use strict';

  angular.module('MODULE')
    .factory('HttpLoadingInterceptor', HttpLoadingInterceptor)
    .config(HttpLoadingInterceptorConfig);

  HttpLoadingInterceptor.$inject = ['$injector', '$q', 'AnalyticsService', '_'];

  HttpLoadingInterceptorConfig.$inject = ['$httpProvider'];

  function HttpLoadingInterceptor ($injector, $q, AnalyticsService, _) {
    var REQUEST_GA_EVENT_NAME = 'REQUEST';

    return {
      request: request,
      requestError: requestError,
      response: response,
      responseError: responseError
    };

    function request (config) {
      config.requestTimestamp = now();
      return config || $q.when(config);
    }

    function requestError (rejection) {
      rejection.config.responseTimestamp = now();
      trackRequest(rejection);
      return $q.reject(rejection);
    }

    function response (response) {
      response.config.responseTimestamp = now();
      trackRequest(response);
      return response || $q.when(response);
    }

    function responseError (rejection) {
      rejection.config.responseTimestamp = now();
      trackRequest(rejection);
      return $q.reject(rejection);
    }

    function trackRequest (response) {
      if (!_.startsWith(response.config.url, 'api')) {
        return;
      }
      AnalyticsService.trackEvent(
        REQUEST_GA_EVENT_NAME,
        response.config.url,
        response.status,
        response.config.responseTimestamp - response.config.requestTimestamp
      );
    }

    function now () {
      return new Date().getTime();
    }
  }

  function HttpLoadingInterceptorConfig ($httpProvider) {
    $httpProvider.interceptors.push('HttpLoadingInterceptor');
  }
})(window.angular);

It looks good at first glance, but comparing the duration of a requests from those collected in the Networks tab in Chrome, I noticed that those gathered in my code are always higher (sometimes much!) from those collected by Chrome.

Another idea that came to my mind, is the use of User Navigation API, so I changed my code to:

(function (angular, performance) {
  'use strict';

  angular.module('MODULE')
    .factory('HttpLoadingInterceptor', HttpLoadingInterceptor)
    .config(HttpLoadingInterceptorConfig);

  HttpLoadingInterceptor.$inject = ['$injector', '$q', 'AnalyticsService', '_'];

  HttpLoadingInterceptorConfig.$inject = ['$httpProvider'];

  function HttpLoadingInterceptor ($injector, $q, AnalyticsService, _) {
    var REQUEST_GA_EVENT_NAME = 'REQUEST';
    var measureReqCnt = 1;

    return {
      request: request,
      requestError: requestError,
      response: response,
      responseError: responseError
    };

    function request (config) {
      if (shouldMeasure(config.url)) {
        measureRequest(config);
      }
      return config || $q.when(config);
    }

    function requestError (rejection) {
      if (shouldMeasure(rejection.config.url)) {
        trackRequest(rejection);
      }
      return $q.reject(rejection);
    }

    function response (response) {
      if (shouldMeasure(response.config.url)) {
        trackRequest(response);
      }
      return response || $q.when(response);
    }

    function responseError (rejection) {
      if (shouldMeasure(rejection.config.url)) {
        trackRequest(rejection);
      }
      return $q.reject(rejection);
    }

    function shouldMeasure (url) {
      return performance && _.startsWith(url, 'api');
    }

    function measureRequest (config) {
      config.measureName = [config.url, measureReqCnt++].join('_');
      config.markStartName = [config.measureName, 'start'].join('_');
      config.markEndName = [config.measureName, 'end'].join('_');
      performance.mark(config.markStartName);
    }

    function trackRequest (response) {
      performance.mark(response.config.markEndName);
      performance.measure(response.config.measureName, response.config.markStartName, response.config.markEndName);
      var entries = performance.getEntriesByName(response.config.measureName, 'measure');
      if (entries && entries.length) {
        AnalyticsService.trackEvent(
          REQUEST_GA_EVENT_NAME,
          response.config.url,
          response.status,
          entries[0].duration
        );
      }
    }
  }

  function HttpLoadingInterceptorConfig ($httpProvider) {
    $httpProvider.interceptors.push('HttpLoadingInterceptor');
  }
})(window.angular, window.performance);

.. but again I received the results different from those collected by Chrome and even other than those measured using the new Date().getTime()

What am I doing wrong? How should I do it right? Maybe Resource Timing API? AngularJS certainly imposes a bit.

I can't use User Navigation API method - window.performacne.getEntries() to find my requests duration, because I can't recognize particular request. Parameters of each request are not available there and I have a lot of the same API calls with different parameters only.

Should I decorate a native XHR request which is used by AngularJS?

1

There are 1 best solutions below

2
On

What about leveraging performance.now()? (https://developer.mozilla.org/en-US/docs/Web/API/Performance/now), which is designed for high precision performance tracking compared to just Date()?

See: performance.now() vs Date.now()

Also, do you care about absolute numbers? For performance tracking, sometimes relative is the best, since comparing apples to oranges rarely gives a real datapoint.

My guess is there is just a discrepancy between both what timer is used and also the points when the time is taken. Chrome Dev tools is probably much closer "to the wire" rather than including application code. If it helps, we also have an API analytics and debugging tool (www.moesif.com/features), but full disclosure that I am the CEO.