1

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?

mrzepinski
  • 419
  • 2
  • 8
  • 21

1 Answers1

0

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.

Community
  • 1
  • 1
Derrick
  • 1,508
  • 11
  • 8
  • Sure, I tried Performance API and I got the same result == no valid result. As I determined with Angular team, Angular gives us his own overhead, so measuring time with all these methods is ineffective. – mrzepinski Feb 01 '17 at 07:23
  • If you don't want any overhead to be included in your numbers, I guess you have to find a way to use `performance.getEntries()` (or the [PerformanceObserver](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver)). Btw, I don't know your exact goal with this measurement call but are you sure you don't want that overhead? Because, it's not just the framework overhead, it could be because your response is too big and parsing that json takes time. I believe that's something you should measure and care about. – inancsevinc Dec 21 '17 at 22:10