I am working on project that uses quite a few js libraries and one of them is outputting awful lot into console, it is polluting the airwaves so bad that it makes it hard to debug....
I know how to disable logging completely by overriding console.log with this,
(function (original) {
console.enableLogging = function () {
console.log = original;
};
console.disableLogging = function () {
console.log = function () {};
};
})(console.log);
but how do it do that per source(file/url) of where message originated?
Preamble
The beginning discusses how stuff works in general. If you just care for the code, skip Introduction and scroll to the Solution heading.
Introduction
Problem:
there is a lot of console noise in a web application. A significant amount of that noise is coming from third party code which we do not have access to. Some of the log noise might be coming from our code, as well.
Requirement:
reduce the noise by stopping the log. Some logs should still be kept and the decision about those should be decoupled from the code that is doing the logging. The granularity needed is "per-file". We should be able to choose which files do or do not add log messages. Finally, this will not be used in production code.
Assumption: this will be ran in a developer controlled browser. In that case, I will not focus on backwards compatibility.
Prior work:
First off logging can be enabled/disabled globally using this
(function (original) {
console.enableLogging = function () {
console.log = original;
};
console.disableLogging = function () {
console.log = function () {};
};
})(console.log);
(code posted in the question but also here for reference)
However, that does not allow for any granularity.
This could be modified to work on only specific modules but that cannot be done for third party code.
A mixed approach would be to disable logging globally but enable it in each of our modules. Problem there is that we have to modify each of our files and we will not get some potentially useful external messages.
A logging framework can be used but it might be an overkill. Although, to be honest, that's what I'd go for, I think, but it may need some integration into the product.
So, we need something light-weight-ish that has some configuration and does not need to be pretty.
Proposal:
The Loginator (title subject to change)
Let's start with the basics - we already know we can override the global log function. We'll take that and work with it. But first, let's recognise that the console object supports more than just .log. There could be various logging functions used. So-o-o, let's disable all of them.
Silence everything
//shorthand for further code.
function noop() {}
const savedFunctions = Object.keys(console)
.reduce((memo, key) => {
if(typeof console[key] == "function") {
//keep a copy just in case we need it
memo[key] = console[key];
//de-fang any functions
console[key] = noop;
}
return memo;
},
{});
console.log("Hello?");
console.info("Hello-o-o-o?");
console.warn("Can anybody hear me?");
console.error("I guess there is nobody there...");
savedFunctions.log("MUAHAHAHA!")
This can obviously be improved but it showcases how any and ll logging can be stopped. In reality, console.error should probably be left and console.warn might be also useful. But this is not the be-all-and-end-all solution.
Next, since we can override console functionality...why not supply our own?
Custom logging
const originalLog = console.log;
console.log = function selectiveHearing() {
if (arguments[0].indexOf("die") !== -1) {
arguments[0] = "Have a nice day!";
}
return originalLog.apply(console, arguments)
}
console.log("Hello.");
console.log("My name is Inigo Montoya.");
console.log("You killed my father.");
console.log("Prepare to die.");
That is all the tools we need to roll our own mini-logging framework.
How to do selective logging
The only thing missing is to determine which file something is coming from. We just need a stack trace.
// The magic
console.log(new Error().stack);
/* SAMPLE:
Error
at Object.module.exports.request (/home/vagrant/src/kumascript/lib/kumascript/caching.js:366:17)
at attempt (/home/vagrant/src/kumascript/lib/kumascript/loaders.js:180:24)
at ks_utils.Class.get (/home/vagrant/src/kumascript/lib/kumascript/loaders.js:194:9)
at /home/vagrant/src/kumascript/lib/kumascript/macros.js:282:24
at /home/vagrant/src/kumascript/node_modules/async/lib/async.js:118:13
at Array.forEach (native)
at _each (/home/vagrant/src/kumascript/node_modules/async/lib/async.js:39:24)
at Object.async.each (/home/vagrant/src/kumascript/node_modules/async/lib/async.js:117:9)
at ks_utils.Class.reloadTemplates (/home/vagrant/src/kumascript/lib/kumascript/macros.js:281:19)
at ks_utils.Class.process (/home/vagrant/src/kumascript/lib/kumascript/macros.js:217:15)
*/
(Relevant bit copied here.)
True, there are some better ways to do it but not a lot. It would either require a framework or it's browser specific - error stacks are not officially supported but they work in Chrome, Edge, and Firefox. Also, come on - it's literally one line - we want simple and don't mind dirty, so I'm happy for the tradeoff.
Solution
Putting it all together. Warning: Do NOT use this in production
(function(whitelist = [], functionsToPreserve = ["error"]) {
function noop() {}
//ensure we KNOW that there is a log function here, just in case
const savedFunctions = { log: console.log }
//proceed with nuking the rest of the chattiness away
Object.keys(console)
.reduce((memo, key) => {
if(typeof console[key] == "function" && functionsToPreserve.indexOf(key) != -1 ) {
memo[key] = console[key];
console[key] = noop;
}
return memo;
},
savedFunctions); //<- it's a const so we can't re-assign it. Besides, we don't need to, if we use it as a seed for reduce()
console.log = function customLog() {
//index 0 - the error message
//index 1 - this function
//index 2 - the calling function, i.e., the actual one that did console.log()
const callingFile = new Error().stack.split("\n")[2];
if (whitelist.some(entry => callingFile.includes(entry))) {
savedFunctions.log.apply(console, arguments)
}
}
})(["myFile.js"]) //hey, it's SOMEWHAT configurable
Or a blacklist
(function(blacklist = [], functionsToPreserve = ["error"]) {
function noop() {}
//ensure we KNOW that there is a log function here, just in case
const savedFunctions = {
log: console.log
}
//proceed with nuking the rest of the chattiness away
Object.keys(console)
.reduce((memo, key) => {
if (typeof console[key] == "function" && functionsToPreserve.indexOf(key) != -1) {
memo[key] = console[key];
console[key] = noop;
}
return memo;
},
savedFunctions); //<- it's a const so we can't re-assign it. Besides, we don't need to, if we use it as a seed for reduce()
console.log = function customLog() {
//index 0 - the error message
//index 1 - this function
//index 2 - the calling function, i.e., the actual one that did console.log()
const callingFile = new Error().stack.split("\n")[2];
if (blacklist.some(entry => callingFile.includes(entry))) {
return;
} else {
savedFunctions.log.apply(console, arguments);
}
}
})(["myFile.js"])
So, this is a custom logger. Sure, it's not perfect but it will do the job. And, hey, since the whitelisting is a bit loose, it could be turned to an advantage:
to whitelist a bunch of files that share a substring, say, all myApp can include myApp1.js, myApp2.js, and myApp3.js.
although if you want specific files, you can just pass the full name, including extension. I doubt there would be a bunch of duplicate filenames.
finally, the stack trace will include the name of the calling function, if any, so you can actually just pass that and that will whitelist on per-function basis. However, it relies on the function having a name and it's more likely for function names to clash, so use with care
Other than that, there can certainly be improvements but that is the basis of it. The info/warn methods can also be overriden, for example.
So, this, if used, should only be in dev builds. There are a lot of ways to make it not go into production, so I won't discuss them but here is one thing I can mention: you can also use this anywhere if you save it as a bookmarklet
javascript:!function(){function c(){}var a=arguments.length<=0||void 0===arguments[0]?[]:arguments[0],b=arguments.length<=1||void 0===arguments[1]?["error"]:arguments[1],d={log:console.log};Object.keys(console).reduce(function(a,d){return"function"==typeof console[d]&&b.indexOf(d)!=-1&&(a[d]=console[d],console[d]=c),a},d),console.log=function(){var c=(new Error).stack.split("\n")[2];a.some(function(a){return c.includes(a)})&&d.log.apply(console,arguments)}}(["myFile.js"]);
This is it minified (although I passed it through Babel first, to use ES5 minification) and still configurable, to an extent, as you can change the very end where you can pass the whitelist. But other than that, it will work the same and is completely decoupled from the codebase. It will not run at pageload but if that's needed you can either use this as a userscript (still decoupled) or include it before other JS files in dev/debug builds only.
A note here - this will work in Chrome, Edge and Firefox. It's all the latest browsers, so I assume a developer will use at least one of them. The question is tagged as Chrome but I decided to widen the support. A Chrome only solution could work slightly better but it's not really a big loss of functionality.
I was as troubled as you. This is my approach. https://github.com/jchnxu/guard-with-debug
Simple usage:
localStorage.debug = [
'enable/console/log/in/this/file.ts',
'enable/console/log/in/this/folder/*',
'-disable/console/log/in/this/file.ts',
'-disable/console/log/in/this/folder/*',
// enable all
'*',
].join(',');
The benefit: it's zero-runtime.
Disclaimer: I am the author of this tiny utility
It work in chrome:
...index.html
<html>
<body>
<script>
(function(){
var original = console.log;
console.log = function(){
var script = document.currentScript;
alert(script.src);
if(script.src === 'file:///C:/Users/degr/Desktop/script.js') {
original.apply(console, arguments)
}
}
})();
console.log('this will be hidden');
</script>
<script src="script.js"></script>
</body>
</html>
...script.js
console.log('this will work');
Console.log does not work from index.html, but work from script.js. Both files situated on my desctop.
I've found these settings in the latest (July 2020) Chrome DevTools console to be helpful:
DevTools | Console | (sidebar icon) | user messages
DevTools | Console | (gear icon) | Select context only
DevTools | Console | (gear icon) | Hide network
I like (1) most, I only see the messages from "my" code. (2) hides messages from my iframe.
If it's an option to modify file, you can set a flag at top of file for disabling logs for that:
var DEBUG = false;
DEBUG && console.log("cyberpunk 2077");
To disable logs for all js files, put it once at top of any js file:
var DEBUG = false;
if (!DEBUG) {
console.log = () => {};
}
This is not pretty but will work.
Put something like this in your file before the <script> tag of the "bad" library :
<script>function GetFile(JSFile) {
var MReq = new XMLHttpRequest();
MReq.open('GET', JSFile, false);
MReq.send();
eval(MReq.responseText.replace(/console.log\(/g,"(function(){})("));
}</script>
Then replace the tag
<script src="badLib.js">
With:
GetFile("badLib.js")
Only for short time debugging.
Working with a POC using Bacon.js and run into a little bit of an issue with Property values.
I am able to retrieve all new property values in the onValue callback however I would like to know what the old property value was before this new value has been set. So far I have not found any easy or elegant solution to achieve this in Bacon out of the box...am I missing something.
Even Object.observe() has a way to get to the old value of the property so surprised I cannot find equivalent behaviour in Bacon.
Would anyone have any suggestions how to handle this? Obviously I do not want to persist the latest property value anywhere in the client code strcily for the sake of being able to do the comparisons between old and new...
You could use slidingwindow to create a new observable with the 2 latest values:
var myProperty = Bacon.sequentially(10, [1,2,3,4,5]) // replace with real property
var slidingWindow = myProperty.startWith(null).slidingWindow(2,2)
slidingWindow.onValues(function(oldValue, newValue) {
// do something with the values
})
Due to lack of the functionality in the native Bacon.js I implemented a custom solution with a wrapper object that holds on to the latest value after it had been forwarded to the subscriber. So instead of tapping into the lens directly client code works with the obsever which also has additional benefits of restricting write access to the model through the lens from client code which is what I needed as well.
function Observer (lens) {
this.lastValue = undefined;
this.lens = lens;
};
Observer.prototype = {
onValue(onValueCallback){
this.subscription = this.lens.onValue(function(newValue) {
onValueCallback(this.lastValue, newValue);
this.lastValue = newValue;
});
},
};
var model = new Bacon.Model({ type : "A"});
var observer = new Observer(model.lens());
observer.onValue(function(oldValue,newValue) { ... });
Background
I just learned that calling keys(window) (or Object.keys(window)) in the DevTools console reveals variables in the global scope (source). I called that code on the StackOverflow page and got this result:
Variable i got my attention as it seems to be in the global scope by a mistake. I tried locating code that is responsible for declaring i, but it turned out to be cumbersome (there is a lot of code and a lot of is).
Question
Getting console warnings that say
Global variable "i" created (main.js:342)
could be useful. How can I implement that feature?
Research
I figured that I need some kind of an event whenever new variable is created.
We do have setters in JavaScript. However, creating a setter requires that you provide a property name. Since I want to monitor all properties I can't really use that.
__noSuchMethod__ (MDN) would be perfect but it only covers methods (and there is no __noSuchProperty__ method).
Object.observe (HTML5 Rocks) doesn't reveal anything about the code that created the property (console.trace() gives me only the name of the observer function).
Object.prototype.watch (MDN) - same as the setter, you have to specify a property name.
Calling Object.preventExtensions(window) (MDN) causes errors with a nice stack trace whenever new global variable is created. The problem with this solution is that it interferes with the script execution and may change its behaviour. It also doesn't allow me to catch the error and format it properly.
Notes
I know about jshint/jslint and I still think that catching these declarations in the runtime could be useful.
I don't care about i variable on the SO page that much, you can probably find the declaration using setters. My question concerns the general solution for this problem.
IMO you have two decent options:
JSHint.
Strict mode.
Both will yell at you when you leak a global. Strict mode will probably be better for your usecases.
You've definitely done your homework, and you thought of all the things I would have thought of and, as you discovered, none of them fit.
The only way I can think of doing is to just monitor the global object (this example is using window as the global object: modify accordingly for Node or another JavaScript container). Here's an example that monitors new globals and deleted globals (if you don't need to monitor deletions, you can remove that functionality):
var globals = Object.keys(window);
var monitorGlobalInterval = 50;
setInterval(function(){
var globalsNow = Object.keys(window);
var newGlobals = globalsNow.filter(function(key){
return globals.indexOf(key)===-1;
});
var deletedGlobals = globals.filter(function(key){
return globalsNow.indexOf(key)===-1;
});
newGlobals.forEach(function(key){
console.log('new global: ' + key);
});
deletedGlobals.forEach(function(key){
console.log('global deleted: ' + key);
});
globals = globalsNow;
}, monitorGlobalInterval);
See it in action here: http://jsfiddle.net/dRjP9/2/
You can try this method to get a list of global variables that you've created:
(function(){
var differences = {},
ignoreList = (prompt('Ignore filter (comma sep)?', 'jQuery, Backbone, _, $').split(/,\s?/) || []),
iframe = document.createElement('iframe'),
count = 0; ignoreList.push('prop');
for (prop in window) {
differences[prop] = {
type: typeof window[prop],
val: window[prop]
}; count++;
}
iframe.src = 'about:blank';
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe = iframe.contentWindow || iframe.contentDocument;
for (prop in differences) {
if (prop in iframe || ignoreList.indexOf(prop) >= 0) {
delete differences[prop];
count--;
}
}
console.info('Total globals: %d', count);
return differences;
})();
I think I may have found a bug with Google Chrome (16.0.912.75 m, the latest stable at the time of this writing).
var FakeFancy = function () {};
console.log(new FakeFancy());
var holder = {
assignTo : function (func) {
holder.constructor = func;
}
};
holder.assignTo(function () { this.type = 'anonymous' });
var globalConstructor = function () { this.type = 'global' }
console.log(new holder.constructor());
If you run that block in Firefox, it shows both listed as "Object" with the second having type = local, pretty good. But if you run it in Chrome, it shows
> FakeFancy
> globalConstructor.type
If you expand the trees, the contents are correct. But I can't figure out what Chrome is listing as the first line for each object logged. Since I'm not manipulating the prototypes, these should be plain old objects that aren't inheriting from anywhere.
At first, I thought it was WebKit related, but I tried in the latest Safari for Windows (5.1.2 7534.52.7) and both show up as "Object".
I suspect that it's attempting to do some guesswork about where the constructor was called from. Is the anonymous constructor's indirection messing it up?
The first line is a result of
console.log(new FakeFancy());
The WebKit console generally tries to do "constructor name inference" to let you know what type of object it's outputting. My guess is that the more recent version included with Chrome (as opposed to Safari 5.1) can do inference for constructor declarations like
var FakeFancy = function () {};
and not just ones like
function FakeFancy() {}
so that's why you're seeing the disparity.
when using JSON.stringify in Google Chrome it seems that toJSON isn't being called? I am using json2.js as a back-up for browsers that don't support it. I guess since Chrome supports JSON but not toJSON json2.js isn't being used at all?
Update
Here is an example: http://jsfiddle.net/GZzvZ/
Firefox: {"foo":"foo","bar":"bar"}
Chrome: {"bar":"bar"}
var t = {};
t.toJSON = function () { alert('meuh'); return (''); }
JSON.stringify(t)
Works perfectly fine for me.
It does alert, so it does call the toJSON method appropriately (in Chrome 8).
EDIT:
That's normal. Your Foo is a function, and function objects are not allowed in JSON. Firefox is just being forgiving, I guess.
Well, actually I wonder, I don't find a clear answer in the standard. Considering you supply a toJSON() to provide your own serialization, should it be allowed or not. But anyway, that's the reason for your failure.
If you edit your jsFiddle example like this, the toJSON is called accordingly on foo.
var obj = function(){
this.foo = 'test'; // OK
//this.foo = function(){ }; KO
this.foo.toJSON = function(){
return 'foo';
};
this.bar = 'bar';
}
var ins = new obj();
var json = JSON.stringify( ins );
document.write( json );