Skip to content

Instantly share code, notes, and snippets.

@cpoDesign
Created August 13, 2016 22:22
Show Gist options
  • Save cpoDesign/6d53ee6af4c94c53510276ca7b27fb15 to your computer and use it in GitHub Desktop.
Save cpoDesign/6d53ee6af4c94c53510276ca7b27fb15 to your computer and use it in GitHub Desktop.
Monitoring $http Activity With $http Interceptors In AngularJS
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Monitoring $http Activity With $http Interceptors In AngularJS
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body ng-controller="AppController">
<h1>
Monitoring $http Activity With $http Interceptors In AngularJS
</h1>
<h2>
All Requests:
</h2>
<ul>
<li>
<strong>Total</strong>: {{ trafficCop.total.all }}
</li>
<li>
<strong>GET</strong>: {{ trafficCop.total.get }}
</li>
<li>
<strong>POST</strong>: {{ trafficCop.total.post }}
</li>
</ul>
<h2>
Pending Requests:
</h2>
<ul>
<li>
<strong>Total</strong>: {{ trafficCop.pending.all }}
</li>
<li>
<strong>GET</strong>: {{ trafficCop.pending.get }}
</li>
<li>
<strong>POST</strong>: {{ trafficCop.pending.post }}
</li>
</ul>
<p>
<a ng-click="makeRequests()">Make more requests</a>.
</p>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/angularjs/angular-1.3.8.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the root of the application.
app.controller(
"AppController",
function setupController( $scope, $http, trafficCop ) {
// Attach the traffic cop directly to the scope so we can more easily
// output the totals (rather than having to set up a watcher to pipe the
// totals out of the service and into the scope for little-to-no benefit).
$scope.trafficCop = trafficCop;
// We can now watch the trafficCop service to see when there are pending
// HTTP requests that we're waiting for.
$scope.$watch(
function calculateModelValue() {
return( trafficCop.pending.all );
},
function handleModelChange( count ) {
console.log(
"Pending HTTP count:", count,
"{",
trafficCop.pending.get, "GET ,",
trafficCop.pending.post, "POST",
"}"
);
}
);
// Initiate some HTTP traffic to observe.
initiateRequests();
// ---
// PUBLIC METHODS.
// ---
// I initiate some more HTTP traffic.
$scope.makeRequests = function() {
initiateRequests();
};
// ---
// PRIVATE METHODS.
// ---
// I spawn HTTP requests.
function initiateRequests() {
$http({
method: "get",
url: "./404.json"
});
$http({
method: "get",
url: "./index.htm"
});
$http({
method: "post",
url: "./404.json"
});
$http({
method: "post",
url: "./index.htm"
});
$http({
method: "get",
url: "./index.htm"
});
$http({
method: "get",
url: "./index.htm"
});
}
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I run during the application bootstrap and hook the $http activity into the
// trafficCop service so we can monitor the activity.
app.config(
function setupConfig( $httpProvider ) {
// Wire up the traffic cop interceptors. This method will be invoked with
// full dependency-injection functionality.
// --
// NOTE: This approach has been available since AngularJS 1.1.4.
$httpProvider.interceptors.push( interceptHttp );
// We're going to TRY to track the outgoing and incoming HTTP requests.
// I stress "TRY" because in a perfect world, this would be very easy
// with the promise-based interceptor chain; but, the world of
// interceptors and data transformations is a cruel she-beast. Any
// interceptor may completely change the outgoing config or the incoming
// response. As such, there's a limit to the accuracy we can provide.
// That said, it is very unlikely that this will break; but, even so, I
// have some work-arounds for unfortunate edge-cases.
function interceptHttp( $q, trafficCop ) {
// Return the interceptor methods. They are all optional and get
// added to the underlying promise chain.
return({
request: request,
requestError: requestError,
response: response,
responseError: responseError
});
// ---
// PUBLIC METHODS.
// ---
// Intercept the request configuration.
function request( config ) {
// NOTE: We know that this config object will contain a method as
// this is the definition of the interceptor - it must accept a
// config object and return a config object.
trafficCop.startRequest( config.method );
// Pass-through original config object.
return( config );
}
// Intercept the failed request.
function requestError( rejection ) {
// At this point, we don't why the outgoing request was rejected.
// And, we may not have access to the config - the rejection may
// be an error object. As such, we'll just track this request as
// a "GET".
// --
// NOTE: We can't ignore this one since our responseError() would
// pick it up and we need to be able to even-out our counts.
trafficCop.startRequest( "get" );
// Pass-through the rejection.
return( $q.reject( rejection ) );
}
// Intercept the successful response.
function response( response ) {
trafficCop.endRequest( extractMethod( response ) );
// Pass-through the resolution.
return( response );
}
// Intercept the failed response.
function responseError( response ) {
trafficCop.endRequest( extractMethod( response ) );
// Pass-through the rejection.
return( $q.reject( response ) );
}
// ---
// PRIVATE METHODS.
// ---
// I attempt to extract the HTTP method from the given response. If
// another interceptor has altered the response (albeit a very
// unlikely occurrence), then we may not be able to access the config
// object or the the underlying method. If this fails, we return GET.
function extractMethod( response ) {
try {
return( response.config.method );
} catch ( error ) {
return( "get" );
}
}
}
}
);
// I keep track of the total number of HTTP requests that have been initiated
// and completed in the application. I work in conjunction with an HTTP
// interceptor that pipes data from the $http service into get/end methods.
app.service(
"trafficCop",
function setupService() {
// I keep track of the total number of HTTP requests that have been
// initiated with the application.
var total = {
all: 0,
get: 0,
post: 0,
delete: 0,
put: 0,
head: 0
};
// I keep track of the total number of HTTP requests that have been
// initiated, but have not yet completed (ie, are still running).
var pending = {
all: 0,
get: 0,
post: 0,
delete: 0,
put: 0,
head: 0
};
// Return the public API.
return({
pending: pending,
total: total,
endRequest: endRequest,
startRequest: startRequest,
});
// ---
// PUBLIC METHODS.
// ---
// I stop tracking the given HTTP request.
function endRequest( httpMethod ) {
httpMethod = normalizedHttpMethod( httpMethod );
pending.all--;
pending[ httpMethod ]--;
// EDGE CASE: In the unlikely event that the interceptors were not
// able to obtain the config object; or, the method was changed after
// our interceptor reached it, there's a chance that our numbers will
// be off. In such a case, we want to try to redistribute negative
// counts onto other properties.
if ( pending[ httpMethod ] < 0 ) {
redistributePendingCounts( httpMethod );
}
}
// I start tracking the given HTTP request.
function startRequest( httpMethod ) {
httpMethod = normalizedHttpMethod( httpMethod );
total.all++;
total[ httpMethod ]++;
pending.all++;
pending[ httpMethod ]++;
}
// ---
// PRIVATE METHODS.
// ---
// I make sure the given HTTP method is recognizable. If it's not, it is
// converted to "get" for consumption.
function normalizedHttpMethod( httpMethod ) {
httpMethod = ( httpMethod || "" ).toLowerCase();
switch ( httpMethod ) {
case "get":
case "post":
case "delete":
case "put":
case "head":
return( httpMethod );
break;
}
return( "get" );
}
// I attempt to redistribute an [unexpected] negative count to other
// non-negative counts. The HTTP methods are iterated in likelihood of
// execution. And, while this isn't an exact science, it will normalize
// after all HTTP requests have finished processing.
function redistributePendingCounts( negativeMethod ) {
var overflow = Math.abs( pending[ negativeMethod ] );
pending[ negativeMethod ] = 0;
// List in likely order of precedence in the application.
var methods = [ "get", "post", "delete", "put", "head" ];
// Trickle the overflow across the list of methods.
for ( var i = 0 ; i < methods.length ; i++ ) {
var method = methods[ i ];
if ( overflow && pending[ method ] ) {
pending[ method ] -= overflow;
if ( pending[ method ] < 0 ) {
overflow = Math.abs( pending[ method ] );
pending[ method ] = 0;
} else {
overflow = 0;
}
}
}
}
}
);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment