Let's say you want to intercept all calls to console.log
, console.warn
, and console.error
, do something sneaky, and then proxy the call back to the original methods so that the messages get printed out as normal and no one ever has to notice. How would you do that?
Attempt #1
If you are a seasoned Javascript programmer, you would probably go to monkeypatching and maybe write something like
function takeOverConsole(){
var original = window.console
window.console = {
log: function(){
// do sneaky stuff
original.log.apply(original, arguments)
}
, warn: function(){
// do sneaky stuff
original.warn.apply(original, arguments)
}
, error: function(){
// do sneaky stuff
original.error.apply(original, arguments)
}
}
}
This works on all browsers except for IE. On IE, console.log
is implemented as a native method, and as such doesn't support the apply
method. This is illustrated if you try to run the following in IE
> console.log.apply(console, ['blah'])
"Object doesn't support property or method 'apply'"
Attempt #2
What to do? Well, for the case of console.log
, we can punt on passing through the variable length arguments exactly as they are, because we already know what the console.log
is going to do with them: join them. Actually the way the join happens varies by browser, but let's just do everyone a favor and make them consistent here by joining them with a space as the separator. Long story short, I ended up with
function takeOverConsole(){
var original = window.console
function handle(method, args){
var message = Array.prototype.slice.apply(args).join(' ')
// do sneaky stuff
if (original) original[method](message)
}
window.console = {
log: function(){
handle('log', arguments)
}
, warn: function(){
handle('warn', arguments)
}
, error: function(){
handle('error', arguments)
}
}
}
This is works everywhere that I have tested, but there is one big flaw - the console object in Chrome and Firebug has more features than just log
, warn
and error
: there're useful things like console.profile
, console.timeStamp
, console.trace
, and lots more. My code effectively removes these extra features - not very gentlemanly.
Attempt #3
So, perhaps instead of replacing the console
object, we should just replace the individual methods we want to intercept. I came up with this
function takeOverConsole(){
var console = window.console
if (!console) return
function intercept(method){
var original = console[method]
console[method] = function(){
var message = Array.prototype.slice.apply(arguments).join(' ')
// do sneaky stuff
original.call(console, message)
}
}
var methods = ['log', 'warn', 'error']
for (var i = 0; i < methods.length; i++)
intercept(methods[i])
}
But this broke on IE again, on the line original.call(console, message)
. The function's call
method, like apply
, is not supported by console.log
. However, curiously - unlike the other browsers - it can be called directly without having its context set to console
, so we can say
original(message) // this works on IE but breaks on Chrome
So the solution as is so often the case is to do one thing on normal browsers, and do another on IE.
function takeOverConsole(){
var console = window.console
if (!console) return
function intercept(method){
var original = console[method]
console[method] = function(){
var message = Array.prototype.slice.apply(arguments).join(' ')
// do sneaky stuff
if (original.call){
// Do this for normal browsers
original.call(console, message)
}else{
// Do this for IE
original(message)
}
}
}
var methods = ['log', 'warn', 'error']
for (var i = 0; i < methods.length; i++)
intercept(methods[i])
}
So that's how it's done! Or not: if you find a flaw in my code, please let me know in the comments!
Update: Attempt #4
Thanks to Jordan Reiter's comment
console.log does not really just concatenate; at least, it doesn't in Firefox, Safari, and Chrome. It keeps them as the objects, so you can expand them and see their children. It'll be immediately obvious that something has been changed. Fix is really easy, though -- IE does concatenate, so use original(message) for IE and original.apply(console, arguments) for Safari, Firefox, Chrome, etc. and then it will be undetectable.
Great catch, Jordan! The improved version is:
function takeOverConsole(){
var console = window.console
if (!console) return
function intercept(method){
var original = console[method]
console[method] = function(){
// do sneaky stuff
if (original.apply){
// Do this for normal browsers
original.apply(console, arguments)
}else{
// Do this for IE
var message = Array.prototype.slice.apply(arguments).join(' ')
original(message)
}
}
}
var methods = ['log', 'warn', 'error']
for (var i = 0; i < methods.length; i++)
intercept(methods[i])
}