Created
August 13, 2016 22:22
-
-
Save cpoDesign/6d53ee6af4c94c53510276ca7b27fb15 to your computer and use it in GitHub Desktop.
Monitoring $http Activity With $http Interceptors In AngularJS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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