Sekrab Garage

Taming the console

Writing a wrapper for console.log for better control in JavaScript, Part I

TipJavaScript May 7, 22
Series:

Console wrapper and error handling

This is not about the different flavors of console logging, this is an attempt to create an organized way to deal with it, in multiple environments in a browser JavaScript app, be it in Angular, Vue, React, or BinCurse. And later make use of it to catch errors in Angular, and create a toast component to display them.
  1. 5 months ago
    Writing a wrapper for console.log for better control in JavaScript, Part I
  2. 5 months ago
    Writing a wrapper for console.log for better control in Angular, Part II
  3. 5 months ago
    Catching and handling errors in Angular
  4. 5 months ago
    Catching and displaying UI errors with toast messages in Angular
  5. 4 months ago
    Auto hiding a toast message in Angular

Why console?

JavaScript is getting old, yet the best way to know what's going on, is a simple console.log in the right place. Read on Chrome Dev site everything you need to know about console. Disclaimer, I find no real value of most of them.

Working with complex frameworks that abstract vanilla JavaScript, we need to log everything in console during development, and that is the main "why".

What to console?

Here are some examples of things that need to be logged:

- Request and response of an HTTP call

- State update

- Initialization sequence

- Configuration ready

- Special third party logging events.

- Temporary attention messages

We also need a way to distinguish general logs, warnings and errors.

Where to console?

We want to disable logs upon environment change, particularly production. Thus the console.log should not be called directly, rather wrapped in another function, that distinguishes environment.

The script

Working backwards, we need a way to do this:

ourFunction(object);

We also want to call a message, but the message is optional:

ourFunction(object, message);

And we want to distinguish the different types of logging:

ourFunction.log(object, message);

ourFunction.info(object, message);

But we do not want to couple it too tightly, so we'll just pass the type as an optional parameter.

ourFunction(object, message, type);

So let's write ourFunction

function ourFunction(o, message, type) {
  // switch type to have different flavors
  console.log(message, o);
}

The first enhancement is obviously the function name. I shall call it _debug. After years of using different names, and clashing with third party, and getting confused with the log, this name withstood the test of times.

Of course, you might argue, why not place it in its own module? You can. I refuse. I want something as dumb as console.log, even dumber.

The second enhancement is adding format to the logs, so they look like this:

colorful log

console.log("%c " + message, style);

As for types, the basic types we want to log are general info (we can add new styles per request), errors and failures, traces, API call results, and warnings.

Third enhancement is to detect a variable before logging, a variable that is set to false in production. And no, this variable is not fed directly from environment, I'll tell you why later. This global constant could be:

window._indebug

Putting it together:

function _debug(o, message, type) {
  if (window && window._indebug) {
    switch (type) {
      case 'e': // error
        console.log('%c ' + message, 'background: red; color: #fff', o);
        break;
      case 't': // trace error
        console.trace('%c ' + message, 'background: red; color: #fff', o);
        break;
      case 'p': // http response
        console.info('%c ' + message, 'background: #222; color: #bada55', o);
        break;
      case 'w': // warning
        console.warn('%c ' + message, 'background: #4f560f; color: #e6ff07', o);
        break;
      case 'gtm': // gtm events, special
        console.info('%cGTM: ' + message, 'background: #03dbfc; color: #000', o);
        break;
      default:
        console.info('%c ' + message, 'background: #d9d9d9; color: #a82868; font-weight: bold;', o);
    }
  }
}

// set _indebug to true in your environment file, could be anywhere
window._inebug = !process.env.production

Example of environment setting in Angular, is in /environment.ts file directly, and since it is in typescript and may run on SSR, check for window, and access it indirectly.

window && (window['_indebug'] = true);

The last enhancement is to add a seperate function, for "attention", that is needed during development. This looks different, and can easily be found and removed from code. Those are not supposed to exist, they are there to make us ditch the habit of ever "console.logging".

function _attn(o, message) {
  if (window && window._indebug) {
    // use debug to filter in console window
    console.debug('%c ' + message, 'background: orange; font-weight: bold; color: black;', o);
  }
}

Typescript

To make it available in typescript, we need to declare it in a typing file, could be typings.d.ts, that is included in the tsconfig. The script is still written in JavaScript, and added to files included in build, because, as you already know by know, it must stay dumb!

// in typing.d.ts
declare function _debug(o: any, message?: string, type?: string): void;
declare function _attn(o: any, message?: string): void;

Using it

The most popular use of custom console logging is intercepting fetch requests. One vanilla way to intercept all fetch calls, is to overwrite fetch. Let's do that:

const oFetch = window.fetch;

window.fetch = async (...args) => {
  const response = await oFetch(...args);
  // let's console here, something like resource url and response
  _debug(response, 'GET ' + args[0], 'p');

  if (!response.ok) {
    // log errors
    _debug(response.statusText, 'Error', 'e');
    return Promise.reject(response);
  }
  return response;
};

// when used in client
fetch('https://saphire.sekrab.com/api/cats')
  .then((response) => response.json())
  .then((json) => {
    // use _attn for temporary logging
    _attn(json, 'after json call');
});

Running in StackBlitz, it looks like this

Console output

Another example, is a call for GTM register event:

function registerEvent(data){
  // ... call dataLayer push, then log
  _debug(data, 'register event', 'gtm');
}

A backdoor

Why did we set a global variable? In an SPA, after first page load, we can set the global variable to true in devTools console, and gain access to these messages on production. It is safe, because it only logs already available information.

For other than SPA, or to make it set to true on load, we can gain access to the backdoor with a URL parameter, again, it's safe, and benign:

const _insearch = window.location.search.indexOf('debug=true') > -1;
if (_insearch) {
    _indebug = true;
}

So now you can share a link with ?debug=true in URL with your team, to gain access to the logged messages, for better insights.

Angular

We can make great use of this function in Angular, logging state changes, HTTP interception, error handling, and more. Come back next week, or may be earlier. 😴

Did I make you look? There is no such thing as BinCurse.

  1. 5 months ago
    Writing a wrapper for console.log for better control in JavaScript, Part I
  2. 5 months ago
    Writing a wrapper for console.log for better control in Angular, Part II
  3. 5 months ago
    Catching and handling errors in Angular
  4. 5 months ago
    Catching and displaying UI errors with toast messages in Angular
  5. 4 months ago
    Auto hiding a toast message in Angular