├── .gitignore ├── .nvmrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── debugout.d.ts ├── debugout.js └── debugout.min.js ├── gulpfile.js ├── index.html ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── debugout.spec.ts └── debugout.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .idea 4 | .DS_Store 5 | .vscode 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### v 1.0.0 (06/27/20) 4 | 5 | - Rewritten in typescript 6 | - Modularized 7 | - More options 8 | - Tests with jest 9 | - Improved logging (multiple args, better formatting) 10 | 11 | ### v 0.5.0 (9/02/14) 12 | 13 | - Added `search()` 14 | - Added `getSlice()` 15 | - Removed logging of `getLog()` 16 | - Improved demo, README 17 | 18 | ### v 0.4.0 (8/26/14) 19 | 20 | - Added Auto-trim feature to keep the log capped at the most recent 2500 lines 21 | - Added `tail()` to just get just the last 100 lines 22 | - Added `downloadLog()` 23 | - Fixed a bug where nested zeroes wouldn't log 24 | 25 | ### v 0.3.0 (6/19/14) 26 | 27 | - Added changelog 28 | - fixed a bug where determineType() would try to evaluate a null object 29 | - added recordLogs option so that log recording could be turned off when going to production, to avoid the log eating up memory. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2016 Inorganik Produce, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | debugout.js 2 | =========== 3 | 4 | (debug output) generates a text file from your logs that can be searched, timestamped, downloaded and more. 5 | 6 | Debugout's `log()` method accepts 1 or more args of any type, including functions. Debugout is not a monkey patch, but a separate logging class altogether that you use in place of `console`. 7 | 8 | Some highlights of debugout: 9 | 10 | - get the entire log, or the tail at runtime or any time 11 | - download the log as a text file 12 | - search and slice the log 13 | - optionally timestamp logs 14 | - also supports info, warn and error methods 15 | - toggle live logging (console.log) in one place 16 | - optionally store the output in `window.localStorage` and continuously add to the same log each session 17 | - optionally cap the log to X most recent lines to limit memory consumption 18 | 19 | **New in 1.0** 20 | 21 | - Improved logging (multiple args, better formatting) 22 | - Modularized 23 | - More options 24 | - Tested with jest 25 | 26 | ## [Try the demo](http://inorganik.github.io/debugout.js/) 27 | 28 | ### Installation 29 | 30 | On npm: `debugout.js` 31 | 32 | ### Usage 33 | 34 | Use as a replacement for `console`, or just use it as a logging utility. 35 | 36 | ```js 37 | import { Debugout } from 'debugout.js'; 38 | 39 | const bugout = new Debugout(); 40 | 41 | // instead of console.log 42 | bugout.log('log stuff', someObject, someArray); 43 | ``` 44 | Whatever you log is saved and added to the log file. 45 | 46 | ### Methods 47 | 48 | - `log()`, `warn()`, `info()`, `error()` - just like `console`, but saved! 49 | - `getLog()` - returns the entire log. 50 | - `tail(numLines)` - returns the last X lines of the log, where X is the number you pass. Defaults to 100. 51 | - `search(string)` - returns numbered lines where there were matches for your search. Pass a string. 52 | - `getSlice(start, end)` - get a 'slice' of the log. Pass the starting line index and optionally an ending line index. 53 | - `downloadLog()` - downloads a .txt file of the log. 54 | - `clear()` - clears the log. 55 | - `determineType()` - a more granular version of `typeof` for your convenience 56 | 57 | ### Options 58 | 59 | Pass any of the following options in the constructor in an object. You can also change them at runtime as properties of your debugout instance. 60 | 61 | ```ts 62 | interface DebugoutOptions { 63 | realTimeLoggingOn?: boolean; // log in real time (forwards to console.log) 64 | useTimestamps?: boolean; // insert a timestamp in front of each log 65 | includeSessionMetadata?: boolean; // whether to include session start, end, duration, and when log is cleared 66 | useLocalStorage?: boolean; // store the output using localStorage and continuously add to the same log each session 67 | recordLogs?: boolean; // disable the core functionality of this lib 68 | autoTrim?: boolean; // to avoid the log eating up potentially endless memory 69 | maxLines?: number; // if autoTrim is true, this many most recent lines are saved 70 | tailNumLines?: number; // default number of lines tail gets 71 | logFilename?: string; // filename of log downloaded with downloadLog() 72 | maxDepth?: number; // max recursion depth for logged objects 73 | localStorageKey?: string; // localStorage key 74 | indent?: string; // string to use for indent 75 | quoteStrings?: boolean; // whether or not to put quotes around strings 76 | } 77 | ``` 78 | Example using options: 79 | 80 | ```js 81 | const bugout = new Debugout({ realTimeLoggingOn: false }); 82 | 83 | // instead of console.log 84 | bugout.log('log stuff'); // real time logging disabled (no console output) 85 | bugout.realTimeLoggingOn = true; 86 | bugout.log('more stuff'); // now, this will show up in your console. 87 | ``` 88 | 89 | ### Usage ideas 90 | 91 | - Post the log to your server if an error or some other event occurs. 92 | - Allow the user to download a copy of a submitted form. 93 | - Generate output for the user to download. 94 | - Record survey answers and know how long each question took the user to answer. 95 | 96 | ### Contributing 97 | 98 | 1. Do your work in src/debugout.ts 99 | 1. Lint `npm run lint` 100 | 1. Test `npm t` 101 | 1. Test demo: `npm start` 102 | 103 | ### Don't log `window` 104 | 105 | Debugout will get stuck in an endless loop if you try to log the window object because it has a circular reference. It will work if you set `maxDepth` to 2, and you can see the root properties. The same thing happens if you do `JSON.stringify(window, null, ' ')`, but the error message provides some insight. 106 | 107 | If you try to log Debugout, it just prints `... (Debugout)`. Otherwise, recursion would cause an endless loop. 108 | 109 | -------------------------------------------------------------------------------- /dist/debugout.d.ts: -------------------------------------------------------------------------------- 1 | export interface DebugoutOptions { 2 | realTimeLoggingOn?: boolean; 3 | useTimestamps?: boolean; 4 | includeSessionMetadata?: boolean; 5 | useLocalStorage?: boolean; 6 | recordLogs?: boolean; 7 | autoTrim?: boolean; 8 | maxLines?: number; 9 | tailNumLines?: number; 10 | logFilename?: string; 11 | maxDepth?: number; 12 | localStorageKey?: string; 13 | indent?: string; 14 | quoteStrings?: boolean; 15 | } 16 | export interface DebugoutStorage { 17 | startTime: string; 18 | log: string; 19 | lastLog: string; 20 | } 21 | export declare class Debugout { 22 | realTimeLoggingOn: boolean; 23 | includeSessionMetadata: boolean; 24 | useTimestamps: boolean; 25 | useLocalStorage: boolean; 26 | recordLogs: boolean; 27 | autoTrim: boolean; 28 | maxLines: number; 29 | logFilename: string; 30 | maxDepth: number; 31 | localStorageKey: string; 32 | indent: string; 33 | quoteStrings: boolean; 34 | tailNumLines: number; 35 | startTime: Date; 36 | output: string; 37 | version: () => string; 38 | indentsForDepth: (depth: number) => string; 39 | trace: () => void; 40 | time: () => void; 41 | timeEnd: () => void; 42 | constructor(options?: DebugoutOptions); 43 | private startLog; 44 | private recordLog; 45 | private logMetadata; 46 | log(...args: unknown[]): void; 47 | info(...args: unknown[]): void; 48 | warn(...args: unknown[]): void; 49 | error(...args: unknown[]): void; 50 | debug(...args: unknown[]): void; 51 | getLog(): string; 52 | clear(): void; 53 | tail(numLines?: number): string; 54 | search(term: string): string; 55 | slice(...args: number[]): string; 56 | downloadLog(): void; 57 | private save; 58 | private load; 59 | determineType(object: any): string; 60 | stringifyObject(obj: any, startingDepth?: number): string; 61 | stringifyArray(arr: Array, startingDepth?: number): string; 62 | stringifyFunction(fn: any, startingDepth?: number): string; 63 | stringify(obj: any, depth?: number): string; 64 | trimLog(maxLines: number): string; 65 | private formatSessionDuration; 66 | formatDate(ts?: Date): string; 67 | objectSize(obj: any): number; 68 | } 69 | -------------------------------------------------------------------------------- /dist/debugout.js: -------------------------------------------------------------------------------- 1 | var __assign = (this && this.__assign) || function () { 2 | __assign = Object.assign || function(t) { 3 | for (var s, i = 1, n = arguments.length; i < n; i++) { 4 | s = arguments[i]; 5 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 6 | t[p] = s[p]; 7 | } 8 | return t; 9 | }; 10 | return __assign.apply(this, arguments); 11 | }; 12 | var debugoutDefaults = { 13 | realTimeLoggingOn: true, 14 | useTimestamps: false, 15 | includeSessionMetadata: true, 16 | useLocalStorage: false, 17 | recordLogs: true, 18 | autoTrim: true, 19 | maxLines: 3000, 20 | tailNumLines: 25, 21 | maxDepth: 20, 22 | logFilename: 'debugout.txt', 23 | localStorageKey: 'debugout.js', 24 | indent: ' ', 25 | quoteStrings: true 26 | }; 27 | var Debugout = /** @class */ (function () { 28 | /* tslint:enable:no-console */ 29 | function Debugout(options) { 30 | var _this = this; 31 | this.indent = ' '; 32 | this.tailNumLines = 25; 33 | this.output = ''; // holds all logs 34 | this.version = function () { return '1.1.0'; }; 35 | this.indentsForDepth = function (depth) { return _this.indent.repeat(Math.max(depth, 0)); }; 36 | // forwarded console methods not used by debugout 37 | /* tslint:disable:no-console */ 38 | this.trace = function () { return console.trace(); }; 39 | this.time = function () { return console.time(); }; 40 | this.timeEnd = function () { return console.timeEnd(); }; 41 | // set options from defaults and passed options. 42 | var settings = __assign(__assign({}, debugoutDefaults), options); 43 | for (var prop in settings) { 44 | if (settings[prop] !== undefined) { 45 | this[prop] = settings[prop]; 46 | } 47 | } 48 | // START/RESUME LOG 49 | if (this.useLocalStorage && window && !!window.localStorage) { 50 | var stored = this.load(); 51 | if (stored) { 52 | this.output = stored.log; 53 | this.startTime = new Date(stored.startTime); 54 | var end = new Date(stored.lastLog); 55 | this.logMetadata("Last session end: " + stored.lastLog); 56 | this.logMetadata("Last " + this.formatSessionDuration(this.startTime, end)); 57 | this.startLog(); 58 | } 59 | else { 60 | this.startLog(); 61 | } 62 | } 63 | else { 64 | this.useLocalStorage = false; 65 | this.startLog(); 66 | } 67 | } 68 | Debugout.prototype.startLog = function () { 69 | this.startTime = new Date(); 70 | this.logMetadata("Session started: " + this.formatDate(this.startTime)); 71 | }; 72 | // records a log 73 | Debugout.prototype.recordLog = function () { 74 | var _this = this; 75 | var args = []; 76 | for (var _i = 0; _i < arguments.length; _i++) { 77 | args[_i] = arguments[_i]; 78 | } 79 | // record log 80 | if (this.useTimestamps) { 81 | this.output += this.formatDate() + ' '; 82 | } 83 | this.output += args.map(function (obj) { return _this.stringify(obj); }).join(' '); 84 | this.output += '\n'; 85 | if (this.autoTrim) 86 | this.output = this.trimLog(this.maxLines); 87 | if (this.useLocalStorage) { 88 | var saveObject = { 89 | startTime: this.startTime, 90 | log: this.output, 91 | lastLog: new Date() 92 | }; 93 | window.localStorage.setItem(this.localStorageKey, JSON.stringify(saveObject)); 94 | } 95 | }; 96 | Debugout.prototype.logMetadata = function (msg) { 97 | if (this.includeSessionMetadata) 98 | this.output += "---- " + msg + " ----\n"; 99 | }; 100 | // USER METHODS 101 | Debugout.prototype.log = function () { 102 | var args = []; 103 | for (var _i = 0; _i < arguments.length; _i++) { 104 | args[_i] = arguments[_i]; 105 | } 106 | if (this.realTimeLoggingOn) 107 | console.log.apply(console, args); 108 | if (this.recordLogs) 109 | this.recordLog.apply(this, args); 110 | }; 111 | Debugout.prototype.info = function () { 112 | var args = []; 113 | for (var _i = 0; _i < arguments.length; _i++) { 114 | args[_i] = arguments[_i]; 115 | } 116 | // tslint:disable-next-line:no-console 117 | if (this.realTimeLoggingOn) 118 | console.info.apply(console, args); 119 | if (this.recordLogs) { 120 | this.output += '[INFO] '; 121 | this.recordLog.apply(this, args); 122 | } 123 | }; 124 | Debugout.prototype.warn = function () { 125 | var args = []; 126 | for (var _i = 0; _i < arguments.length; _i++) { 127 | args[_i] = arguments[_i]; 128 | } 129 | if (this.realTimeLoggingOn) 130 | console.warn.apply(console, args); 131 | if (this.recordLogs) { 132 | this.output += '[WARN] '; 133 | this.recordLog.apply(this, args); 134 | } 135 | }; 136 | Debugout.prototype.error = function () { 137 | var args = []; 138 | for (var _i = 0; _i < arguments.length; _i++) { 139 | args[_i] = arguments[_i]; 140 | } 141 | if (this.realTimeLoggingOn) 142 | console.error.apply(console, args); 143 | if (this.recordLogs) { 144 | this.output += '[ERROR] '; 145 | this.recordLog.apply(this, args); 146 | } 147 | }; 148 | Debugout.prototype.debug = function () { 149 | var args = []; 150 | for (var _i = 0; _i < arguments.length; _i++) { 151 | args[_i] = arguments[_i]; 152 | } 153 | if (this.realTimeLoggingOn) 154 | console.debug.apply(console, args); 155 | if (this.recordLogs) { 156 | this.output += '[DEBUG] '; 157 | this.recordLog.apply(this, args); 158 | } 159 | }; 160 | Debugout.prototype.getLog = function () { 161 | var retrievalTime = new Date(); 162 | // if recording is off, so dev knows why they don't have any logs 163 | if (!this.recordLogs) { 164 | this.info('Log recording is off'); 165 | } 166 | // if using local storage, get values 167 | if (this.useLocalStorage && window && window.localStorage) { 168 | var stored = this.load(); 169 | if (stored) { 170 | this.startTime = new Date(stored.startTime); 171 | this.output = stored.log; 172 | } 173 | } 174 | if (this.includeSessionMetadata) { 175 | return this.output + ("---- " + this.formatSessionDuration(this.startTime, retrievalTime) + " ----\n"); 176 | } 177 | return this.output; 178 | }; 179 | // clears the log 180 | Debugout.prototype.clear = function () { 181 | this.output = ''; 182 | this.logMetadata("Session started: " + this.formatDate(this.startTime)); 183 | this.logMetadata('Log cleared ' + this.formatDate()); 184 | if (this.useLocalStorage) 185 | this.save(); 186 | }; 187 | // gets last X number of lines 188 | Debugout.prototype.tail = function (numLines) { 189 | var lines = numLines || this.tailNumLines; 190 | return this.trimLog(lines); 191 | }; 192 | // find occurences of your search term in the log 193 | Debugout.prototype.search = function (term) { 194 | var rgx = new RegExp(term, 'ig'); 195 | var lines = this.output.split('\n'); 196 | var matched = []; 197 | // can't use a simple filter & map here because we need to add the line number 198 | for (var i = 0; i < lines.length; i++) { 199 | var addr = "[" + i + "] "; 200 | if (lines[i].match(rgx)) { 201 | matched.push(addr + lines[i].trim()); 202 | } 203 | } 204 | var result = matched.join('\n'); 205 | if (!result.length) 206 | result = "Nothing found for \"" + term + "\"."; 207 | return result; 208 | }; 209 | // retrieve a section of the log. Works the same as js slice 210 | Debugout.prototype.slice = function () { 211 | var _a; 212 | var args = []; 213 | for (var _i = 0; _i < arguments.length; _i++) { 214 | args[_i] = arguments[_i]; 215 | } 216 | return (_a = this.output.split('\n')).slice.apply(_a, args).join('\n'); 217 | }; 218 | // downloads the log - for browser use 219 | Debugout.prototype.downloadLog = function () { 220 | if (!!window) { 221 | var logFile = this.getLog(); 222 | var blob = new Blob([logFile], { type: 'data:text/plain;charset=utf-8' }); 223 | var a = document.createElement('a'); 224 | a.href = window.URL.createObjectURL(blob); 225 | a.target = '_blank'; 226 | a.download = this.logFilename; 227 | document.body.appendChild(a); 228 | a.click(); 229 | document.body.removeChild(a); 230 | window.URL.revokeObjectURL(a.href); 231 | } 232 | else { 233 | console.error('downloadLog only works in the browser'); 234 | } 235 | }; 236 | // METHODS FOR CONSTRUCTING THE LOG 237 | Debugout.prototype.save = function () { 238 | var saveObject = { 239 | startTime: this.startTime, 240 | log: this.output, 241 | lastLog: new Date() 242 | }; 243 | window.localStorage.setItem(this.localStorageKey, JSON.stringify(saveObject)); 244 | }; 245 | Debugout.prototype.load = function () { 246 | var saved = window.localStorage.getItem(this.localStorageKey); 247 | if (saved) { 248 | return JSON.parse(saved); 249 | } 250 | return null; 251 | }; 252 | Debugout.prototype.determineType = function (object) { 253 | if (object === null) { 254 | return 'null'; 255 | } 256 | else if (object === undefined) { 257 | return 'undefined'; 258 | } 259 | else { 260 | var type = typeof object; 261 | if (type === 'object') { 262 | if (Array.isArray(object)) { 263 | type = 'Array'; 264 | } 265 | else { 266 | if (object instanceof Date) { 267 | type = 'Date'; 268 | } 269 | else if (object instanceof RegExp) { 270 | type = 'RegExp'; 271 | } 272 | else if (object instanceof Debugout) { 273 | type = 'Debugout'; 274 | } 275 | else { 276 | type = 'Object'; 277 | } 278 | } 279 | } 280 | return type; 281 | } 282 | }; 283 | // recursively stringify object 284 | Debugout.prototype.stringifyObject = function (obj, startingDepth) { 285 | if (startingDepth === void 0) { startingDepth = 0; } 286 | // return JSON.stringify(obj, null, this.indent); // can't control depth/line-breaks/quotes 287 | var result = '{'; 288 | var depth = startingDepth; 289 | if (this.objectSize(obj) > 0) { 290 | result += '\n'; 291 | depth++; 292 | var i = 0; 293 | for (var prop in obj) { 294 | result += this.indentsForDepth(depth); 295 | result += prop + ': '; 296 | var subresult = this.stringify(obj[prop], depth); 297 | if (subresult) { 298 | result += subresult; 299 | } 300 | if (i < this.objectSize(obj) - 1) 301 | result += ','; 302 | result += '\n'; 303 | i++; 304 | } 305 | depth--; 306 | result += this.indentsForDepth(depth); 307 | } 308 | result += '}'; 309 | return result; 310 | }; 311 | // recursively stringify array 312 | Debugout.prototype.stringifyArray = function (arr, startingDepth) { 313 | if (startingDepth === void 0) { startingDepth = 0; } 314 | // return JSON.stringify(arr, null, this.indent); // can't control depth/line-breaks/quotes 315 | var result = '['; 316 | var depth = startingDepth; 317 | var lastLineNeedsNewLine = false; 318 | if (arr.length > 0) { 319 | depth++; 320 | for (var i = 0; i < arr.length; i++) { 321 | var subtype = this.determineType(arr[i]); 322 | var needsNewLine = false; 323 | if (subtype === 'Object' && this.objectSize(arr[i]) > 0) 324 | needsNewLine = true; 325 | if (subtype === 'Array' && arr[i].length > 0) 326 | needsNewLine = true; 327 | if (!lastLineNeedsNewLine && needsNewLine) 328 | result += '\n'; 329 | var subresult = this.stringify(arr[i], depth); 330 | if (subresult) { 331 | if (needsNewLine) 332 | result += this.indentsForDepth(depth); 333 | result += subresult; 334 | if (i < arr.length - 1) 335 | result += ', '; 336 | if (needsNewLine) 337 | result += '\n'; 338 | } 339 | lastLineNeedsNewLine = needsNewLine; 340 | } 341 | depth--; 342 | } 343 | result += ']'; 344 | return result; 345 | }; 346 | // pretty-printing functions is a lib unto itself - this simply prints with indents 347 | Debugout.prototype.stringifyFunction = function (fn, startingDepth) { 348 | var _this = this; 349 | if (startingDepth === void 0) { startingDepth = 0; } 350 | var depth = startingDepth; 351 | return String(fn).split('\n').map(function (line) { 352 | if (line.match(/\}/)) 353 | depth--; 354 | var val = _this.indentsForDepth(depth) + line.trim(); 355 | if (line.match(/\{/)) 356 | depth++; 357 | return val; 358 | }).join('\n'); 359 | }; 360 | // stringify any data 361 | Debugout.prototype.stringify = function (obj, depth) { 362 | if (depth === void 0) { depth = 0; } 363 | if (depth >= this.maxDepth) { 364 | return '... (max-depth reached)'; 365 | } 366 | var type = this.determineType(obj); 367 | switch (type) { 368 | case 'Object': 369 | return this.stringifyObject(obj, depth); 370 | case 'Array': 371 | return this.stringifyArray(obj, depth); 372 | case 'function': 373 | return this.stringifyFunction(obj, depth); 374 | case 'RegExp': 375 | return '/' + obj.source + '/' + obj.flags; 376 | case 'Date': 377 | case 'string': 378 | return (this.quoteStrings) ? "\"" + obj + "\"" : obj + ''; 379 | case 'boolean': 380 | return (obj) ? 'true' : 'false'; 381 | case 'number': 382 | return obj + ''; 383 | case 'null': 384 | case 'undefined': 385 | return type; 386 | case 'Debugout': 387 | return '... (Debugout)'; // prevent endless loop 388 | default: 389 | return '?'; 390 | } 391 | }; 392 | Debugout.prototype.trimLog = function (maxLines) { 393 | var lines = this.output.split('\n'); 394 | lines.pop(); 395 | if (lines.length > maxLines) { 396 | lines = lines.slice(lines.length - maxLines); 397 | } 398 | return lines.join('\n') + '\n'; 399 | }; 400 | // no type args: typescript doesn't think dates can be subtracted but they can 401 | Debugout.prototype.formatSessionDuration = function (startTime, endTime) { 402 | var msec = endTime - startTime; 403 | var hh = Math.floor(msec / 1000 / 60 / 60); 404 | var hrs = ('0' + hh).slice(-2); 405 | msec -= hh * 1000 * 60 * 60; 406 | var mm = Math.floor(msec / 1000 / 60); 407 | var mins = ('0' + mm).slice(-2); 408 | msec -= mm * 1000 * 60; 409 | var ss = Math.floor(msec / 1000); 410 | var secs = ('0' + ss).slice(-2); 411 | msec -= ss * 1000; 412 | return 'Session duration: ' + hrs + ':' + mins + ':' + secs; 413 | }; 414 | Debugout.prototype.formatDate = function (ts) { 415 | if (ts === void 0) { ts = new Date(); } 416 | return "[" + ts.toISOString() + "]"; 417 | }; 418 | Debugout.prototype.objectSize = function (obj) { 419 | var size = 0; 420 | for (var key in obj) { 421 | if (obj.hasOwnProperty(key)) 422 | size++; 423 | } 424 | return size; 425 | }; 426 | return Debugout; 427 | }()); 428 | export { Debugout }; 429 | -------------------------------------------------------------------------------- /dist/debugout.min.js: -------------------------------------------------------------------------------- 1 | var __assign=this&&this.__assign||function(){return(__assign=Object.assign||function(t){for(var e,o=1,i=arguments.length;o0){o+="\n",i++;var r=0;for(var n in t){o+=this.indentsForDepth(i),o+=n+": ";var s=this.stringify(t[n],i);s&&(o+=s),r0){i++;for(var n=0;n0&&(a=!0),"Array"===s&&t[n].length>0&&(a=!0),!r&&a&&(o+="\n");var u=this.stringify(t[n],i);u&&(a&&(o+=this.indentsForDepth(i)),o+=u,n=this.maxDepth)return"... (max-depth reached)";var o=this.determineType(t);switch(o){case"Object":return this.stringifyObject(t,e);case"Array":return this.stringifyArray(t,e);case"function":return this.stringifyFunction(t,e);case"RegExp":return"/"+t.source+"/"+t.flags;case"Date":case"string":return this.quoteStrings?'"'+t+'"':t+"";case"boolean":return t?"true":"false";case"number":return t+"";case"null":case"undefined":return o;case"Debugout":return"... (Debugout)";default:return"?"}},t.prototype.trimLog=function(t){var e=this.output.split("\n");return e.pop(),e.length>t&&(e=e.slice(e.length-t)),e.join("\n")+"\n"},t.prototype.formatSessionDuration=function(t,e){var o=e-t,i=Math.floor(o/1e3/60/60),r=("0"+i).slice(-2);o-=1e3*i*60*60;var n=Math.floor(o/1e3/60),s=("0"+n).slice(-2);o-=1e3*n*60;var a=Math.floor(o/1e3);return o-=1e3*a,"Session duration: "+r+":"+s+":"+("0"+a).slice(-2)},t.prototype.formatDate=function(t){return void 0===t&&(t=new Date),"["+t.toISOString()+"]"},t.prototype.objectSize=function(t){var e=0;for(var o in t)t.hasOwnProperty(o)&&e++;return e},t}();export{Debugout}; -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const uglifyES = require('uglify-es'); 3 | const composer = require('gulp-uglify/composer'); 4 | const concat = require('gulp-concat'); 5 | const del = require('del'); 6 | const uglify = composer(uglifyES, console); 7 | 8 | const clean = () => del(['dist/*']); 9 | 10 | const build = () => { 11 | return gulp.src('./dist/debugout.js') 12 | .pipe(concat('debugout.min.js')) 13 | .pipe(uglify()) 14 | .pipe(gulp.dest('dist')); 15 | } 16 | 17 | gulp.task('clean', clean); 18 | gulp.task('build', build); 19 | 20 | exports.clean = clean; 21 | exports.default = build; 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Debugout.js 12 | 13 | 14 | 15 | 28 | 29 | 30 | 31 | Fork me on GitHub 33 |
34 |
35 |
36 |
37 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 38 |
39 | 40 |
41 |
42 |

debugout.js  

43 |

debugout.js (debug output) generates a text file from your logs that can be searched, timestamped, 44 | downloaded and more. See examples and usage on GitHub.

45 |

Install via npm/yarn using the package name  debugout.js.

46 |

Download on GitHub

48 |
49 |
50 |
51 |

Create a new debugout object and replace "console" with your instance of Debugout. Try it in your console using "bugout" instead of "console".

52 |

The following logs (in purple) produce the output (in green) below:

53 |
54 |
55 |
56 |
57 |
58 | const bugout = new Debugout();
59 | bugout.warn('a warning');
60 | bugout.error('an error');
61 | bugout.log('A date:', d);
62 | bugout.log('An array:', arr);
63 | bugout.log('A function:', testFunc);
64 | bugout.log('A regex', rgx);
65 | bugout.log('an object with nested objects and arrays...', obj);
66 | bugout.log('an array of objects...', arrayWithObjects);
67 |
68 |
69 |
70 |
71 |
72 |
73 |

Get the log at run-time or any time with these 74 | methods:

75 | 76 | 77 | 78 |
79 |
80 |
81 |
82 |

Also, search:

83 | 85 | 86 | 87 |

And slice:

88 | 90 | 92 | 93 |

Also:

94 | 95 |
96 |
97 |
98 |
99 |

Options:

100 |
101 | 102 |
103 |
104 | 105 |
106 |
107 | 108 |
109 |
110 | 111 |
112 |
113 | 114 |
115 |
116 | 117 |
118 |
119 | 120 |
121 |
122 | 123 | 124 |
125 |
126 | 127 | 128 |
129 |
130 | 131 | 132 |
133 |
134 | 135 | 136 |
137 |
138 | 139 | 140 |
141 |
142 |
143 |
144 |
145 |

Console window:

146 |
147 | // Use a method above and output will be displayed here 148 |
149 |
150 |
151 |
152 | 153 | 154 | 275 | 276 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [ 3 | '/src' 4 | ], 5 | preset: 'ts-jest', 6 | testEnvironment: 'node', 7 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "debugout.js", 3 | "version": "1.1.0", 4 | "description": "Generates a text file from your logs.", 5 | "main": "dist/debugout.min.js", 6 | "scripts": { 7 | "build": "npm run clean && tsc && gulp", 8 | "clean": "gulp clean", 9 | "lint": "tslint --project tsconfig.json", 10 | "serve": "http-server ./", 11 | "start": "npm run build && npm run serve", 12 | "test": "jest", 13 | "test:watch": "jest --watch" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/inorganik/debugout.js.git" 18 | }, 19 | "keywords": [ 20 | "logging", 21 | "debug", 22 | "log", 23 | "txt" 24 | ], 25 | "author": "@inorganik", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/inorganik/debugout.js/issues" 29 | }, 30 | "homepage": "http://inorganik.github.io/debugout.js/", 31 | "devDependencies": { 32 | "@types/jest": "^26.0.24", 33 | "del": "^5.1.0", 34 | "gulp": "^4.0.2", 35 | "gulp-concat": "^2.6.1", 36 | "gulp-uglify": "^3.0.2", 37 | "http-server": "^0.12.3", 38 | "jest": "^26.6.3", 39 | "ts-jest": "^26.5.6", 40 | "tslint": "^6.1.3", 41 | "typescript": "^3.9.10", 42 | "uglify-es": "^3.3.9" 43 | }, 44 | "types": "./dist/debugout.d.ts" 45 | } 46 | -------------------------------------------------------------------------------- /src/debugout.spec.ts: -------------------------------------------------------------------------------- 1 | import { Debugout } from './debugout'; 2 | 3 | describe('Debugout', () => { 4 | 5 | let debugout; 6 | const subjects = { 7 | string: 'A string', 8 | number: 26.2, 9 | boolean: false, 10 | date: new Date('2020-06-26T10:33:15Z'), 11 | array: [0, 'string', {}], 12 | object: { a: 'apple', b: 2, c: {}, d: [] }, 13 | function: () => 'it works', 14 | regex: new RegExp(/debugout/gi), 15 | null: null, 16 | undef: undefined 17 | }; 18 | 19 | describe('# determineType', () => { 20 | 21 | beforeEach(() => { 22 | debugout = new Debugout({ realTimeLoggingOn: false }); 23 | }); 24 | 25 | it('should properly type a string', () => { 26 | expect(debugout.determineType(subjects.string)).toEqual('string'); 27 | }); 28 | it('should properly type a number', () => { 29 | expect(debugout.determineType(subjects.number)).toEqual('number'); 30 | }); 31 | it('should properly type a boolean', () => { 32 | expect(debugout.determineType(subjects.boolean)).toEqual('boolean'); 33 | }); 34 | it('should properly type a date', () => { 35 | expect(debugout.determineType(subjects.date)).toEqual('Date'); 36 | }); 37 | it('should properly type an array', () => { 38 | expect(debugout.determineType(subjects.array)).toEqual('Array'); 39 | }); 40 | it('should properly type an object', () => { 41 | expect(debugout.determineType(subjects.object)).toEqual('Object'); 42 | }); 43 | it('should properly type a function', () => { 44 | expect(debugout.determineType(subjects.function)).toEqual('function'); 45 | }); 46 | it('should properly type a regex', () => { 47 | expect(debugout.determineType(subjects.regex)).toEqual('RegExp'); 48 | }); 49 | it('should properly type null', () => { 50 | expect(debugout.determineType(subjects.null)).toEqual('null'); 51 | }); 52 | it('should properly type undefined', () => { 53 | expect(debugout.determineType(subjects.undef)).toEqual('undefined'); 54 | }); 55 | }); 56 | 57 | describe('# indentsForDepth', () => { 58 | 59 | beforeEach(() => { 60 | debugout = new Debugout({ realTimeLoggingOn: false }); 61 | }); 62 | 63 | it('should work', () => { 64 | expect(debugout.indentsForDepth(1)).toEqual(' '); 65 | expect(debugout.indentsForDepth(2)).toEqual(' '); 66 | expect(debugout.indentsForDepth(3)).toEqual(' '); 67 | }); 68 | }); 69 | 70 | describe('# stringify', () => { 71 | 72 | beforeEach(() => { 73 | debugout = new Debugout({ realTimeLoggingOn: false }); 74 | }); 75 | 76 | it('should properly stringify a string', () => { 77 | expect(debugout.stringify(subjects.string)).toEqual('"A string"'); 78 | }); 79 | it('should properly stringify a number', () => { 80 | expect(debugout.stringify(subjects.number)).toEqual('26.2'); 81 | }); 82 | it('should properly stringify a boolean', () => { 83 | expect(debugout.stringify(subjects.boolean)).toEqual('false'); 84 | }); 85 | it('should properly stringify a date', () => { 86 | const result = debugout.stringify(subjects.date); 87 | expect(result.substring(0, 16)).toEqual('"Fri Jun 26 2020'); 88 | }); 89 | it('should properly stringify a regex', () => { 90 | expect(debugout.stringify(subjects.regex)).toEqual('/debugout/gi'); 91 | }); 92 | it('should properly stringify null', () => { 93 | expect(debugout.stringify(subjects.null)).toEqual('null'); 94 | }); 95 | it('should properly stringify undefined', () => { 96 | expect(debugout.stringify(subjects.undef)).toEqual('undefined'); 97 | }); 98 | it('should detect itself', () => { 99 | expect(debugout.stringify(debugout)).toEqual('... (Debugout)'); 100 | }); 101 | 102 | describe('Stringifying arrays', () => { 103 | it('should properly stringify an array', () => { 104 | const expected = '[0, "string", {}]'; 105 | expect(debugout.stringify(subjects.array)).toEqual(expected); 106 | }); 107 | it('can handle arrays nested in arrays', () => { 108 | const subj = [subjects.array]; 109 | const nested = '[0, "string", {}]'; 110 | const expected = `[\n ${nested}\n]`; 111 | expect(debugout.stringify(subj)).toEqual(expected); 112 | }); 113 | it('can handle objects nested in arrays', () => { 114 | const subj = [subjects.object]; 115 | const nested = '{\n a: "apple",\n b: 2,\n c: {},\n d: []\n }'; 116 | const expected = `[\n ${nested}\n]`; 117 | expect(debugout.stringify(subj)).toEqual(expected); 118 | }); 119 | it('properly stringifies an array of objects', () => { 120 | const obj = { apple: 'red', banana: 'yellow' }; 121 | const expectedObj = ' {\n apple: "red",\n banana: "yellow"\n }'; 122 | expect(debugout.stringify([obj, obj])).toEqual('[\n' + expectedObj + ', \n' + expectedObj + '\n]'); 123 | }); 124 | }); 125 | 126 | describe('Stringifying objects', () => { 127 | it('should properly stringify an object', () => { 128 | const expected = '{\n a: "apple",\n b: 2,\n c: {},\n d: []\n}'; 129 | expect(debugout.stringify(subjects.object)).toEqual(expected); 130 | }); 131 | it('can handle objects nested in objects', () => { 132 | const subj = { nested: subjects.object }; 133 | const nested = '{\n a: "apple",\n b: 2,\n c: {},\n d: []\n }'; 134 | const expected = `{\n nested: ${nested}\n}`; 135 | expect(debugout.stringify(subj)).toEqual(expected); 136 | }); 137 | it('can handle arrays nested in objects', () => { 138 | const subj = { nested: subjects.array }; 139 | const nested = '[0, "string", {}]'; 140 | const expected = `{\n nested: ${nested}\n}`; 141 | expect(debugout.stringify(subj)).toEqual(expected); 142 | }); 143 | }); 144 | 145 | describe('Stringifying functions', () => { 146 | it('should properly stringify a 1-line function', () => { 147 | expect(debugout.stringify(subjects.function)).toEqual('function () { return \'it works\'; }'); 148 | }); 149 | it('should properly stringify a multi-line function', () => { 150 | /* tslist:disable */ 151 | function objectSize(obj) { 152 | var size = 0; 153 | for (var key in obj) { 154 | if (obj.hasOwnProperty(key)) size++; 155 | } 156 | return size; 157 | } 158 | /* tslint:enable */ 159 | const expected = 'function objectSize(obj) {\n' 160 | + ' var size = 0;\n' 161 | + ' for (var key in obj) {\n' 162 | + ' if (obj.hasOwnProperty(key))\n' 163 | + ' size++;\n' 164 | + ' }\n' 165 | + ' return size;\n' 166 | + '}'; 167 | expect(debugout.stringify(objectSize)).toEqual(expected); 168 | }); 169 | }); 170 | 171 | }); 172 | 173 | describe('# log', () => { 174 | 175 | it('caches logs in memory', () => { 176 | debugout = new Debugout({ includeSessionMetadata: false, realTimeLoggingOn: false }); 177 | debugout.log('a test'); 178 | const result = debugout.getLog(); 179 | expect(result).toEqual('"a test"\n'); 180 | }); 181 | 182 | it('can handle multiple args', () => { 183 | debugout = new Debugout({ includeSessionMetadata: false, realTimeLoggingOn: false }); 184 | debugout.log('a string', subjects.string); 185 | debugout.log('2 numbers', 26.2, 98.6); 186 | const results = debugout.getLog().split('\n'); 187 | expect(results[0]).toEqual('"a string" "A string"'); 188 | expect(results[1]).toEqual('"2 numbers" 26.2 98.6'); 189 | }); 190 | }); 191 | 192 | describe('# getLog', () => { 193 | 194 | it('gets the log', () => { 195 | debugout = new Debugout({ includeSessionMetadata: false, realTimeLoggingOn: false }); 196 | debugout.log('a string', subjects.string); 197 | const result = debugout.getLog(); 198 | expect(result.length).toBeGreaterThan(0); 199 | }); 200 | }); 201 | 202 | describe('# clear', () => { 203 | 204 | it('clears the log', () => { 205 | debugout = new Debugout({ includeSessionMetadata: false, realTimeLoggingOn: false }); 206 | debugout.log('a string', subjects.string); 207 | debugout.clear(); 208 | const result = debugout.getLog(); 209 | expect(result.length).toEqual(0); 210 | }); 211 | }); 212 | 213 | describe('# tail', () => { 214 | 215 | it('gets the tail', () => { 216 | debugout = new Debugout({ includeSessionMetadata: false, realTimeLoggingOn: false }); 217 | debugout.log('a string', subjects.string); 218 | debugout.log('a number', 26.2); 219 | debugout.log('a number', 98.6); 220 | expect(debugout.tail(2)).toEqual('"a number" 26.2\n"a number" 98.6\n'); 221 | }); 222 | }); 223 | 224 | describe('# search', () => { 225 | 226 | it('finds all occurences of a search term', () => { 227 | debugout = new Debugout({ includeSessionMetadata: false, realTimeLoggingOn: false }); 228 | debugout.log('zebra, giraffe, gorilla'); 229 | debugout.log('jeep, moab, utah'); 230 | debugout.log('apple, orange, banana'); 231 | debugout.log('hells revenge, fins n things, moab'); 232 | expect(debugout.search('Moab')).toEqual('[1] "jeep, moab, utah"\n[3] "hells revenge, fins n things, moab"'); 233 | }); 234 | }); 235 | 236 | describe('# slice', () => { 237 | 238 | it('gets a slice', () => { 239 | debugout.log('zebra, giraffe, gorilla'); 240 | debugout.log('jeep, moab, utah'); 241 | debugout.log('apple, orange, banana'); 242 | debugout.log('hells revenge, fins n things, moab'); 243 | expect(debugout.slice(1, 3)).toEqual('"jeep, moab, utah"\n"apple, orange, banana"'); 244 | }); 245 | }); 246 | 247 | describe('options', () => { 248 | 249 | let consoleSpy; 250 | beforeEach(() => { 251 | debugout = new Debugout({ includeSessionMetadata: false }); 252 | consoleSpy = jest.spyOn(console, 'log'); 253 | }); 254 | 255 | it('should respect "realTimeLoggingOn"', () => { 256 | debugout.realTimeLoggingOn = true; 257 | debugout.log('zebra'); 258 | expect(consoleSpy).toHaveBeenCalled(); 259 | consoleSpy.mockReset(); 260 | 261 | debugout.realTimeLoggingOn = false; 262 | debugout.log('zebra'); 263 | expect(consoleSpy).not.toHaveBeenCalled(); 264 | consoleSpy.mockReset(); 265 | }); 266 | 267 | it('should respect "useTimestamps"', () => { 268 | debugout = new Debugout({ useTimestamps: true, includeSessionMetadata: false }); 269 | debugout.log('zebra'); 270 | expect(debugout.getLog().length).toBeGreaterThan(15); 271 | 272 | debugout = new Debugout({ useTimestamps: false, includeSessionMetadata: false }); 273 | debugout.log('zebra'); 274 | expect(debugout.getLog()).toEqual('"zebra"\n'); 275 | }); 276 | 277 | it('should respect "includeSessionMetadata"', () => { 278 | debugout = new Debugout({ includeSessionMetadata: true, realTimeLoggingOn: false }); 279 | debugout.log('zebra'); 280 | expect(debugout.getLog().match(/^---- Session/)).toBeTruthy(); 281 | }); 282 | 283 | it('should respect "recordLogs"', () => { 284 | debugout.recordLogs = false; 285 | debugout.log('zebra'); 286 | expect(debugout.getLog().length).toEqual(0); 287 | }); 288 | 289 | it('should respect "maxLines"', () => { 290 | debugout.maxLines = 1; 291 | debugout.log('a string', subjects.string); 292 | debugout.log('a number', 26.2); 293 | debugout.log('a number', 98.6); 294 | expect(debugout.getLog()).toEqual('"a number" 98.6\n'); 295 | }); 296 | 297 | it('should respect "maxDepth"', () => { 298 | debugout.maxDepth = 1; 299 | debugout.log({ nested: { nested: { nested: { nested: 'hi' }}}}); 300 | const expected = '{\n nested: ... (max-depth reached)\n}\n'; 301 | expect(debugout.getLog()).toEqual(expected); 302 | }); 303 | 304 | it('should respect "indent"', () => { 305 | debugout.indent = '--'; 306 | debugout.log({ nested: { message: 'hi' }}); 307 | const expected = '{\n--nested: {\n----message: "hi"\n--}\n}\n'; 308 | expect(debugout.getLog()).toEqual(expected); 309 | }); 310 | 311 | it('should respect "quoteStrings"', () => { 312 | debugout.quoteStrings = false; 313 | debugout.log('zebra'); 314 | expect(debugout.getLog()).toEqual('zebra\n'); 315 | }); 316 | }); 317 | 318 | }); 319 | -------------------------------------------------------------------------------- /src/debugout.ts: -------------------------------------------------------------------------------- 1 | export interface DebugoutOptions { 2 | realTimeLoggingOn?: boolean; // log in real time (forwards to console.log) 3 | useTimestamps?: boolean; // insert a timestamp in front of each log 4 | includeSessionMetadata?: boolean; // whether to include session start, end, duration, and when log is cleared 5 | useLocalStorage?: boolean; // store the output using localStorage and continuously add to the same log each session 6 | recordLogs?: boolean; // disable the core functionality of this lib 7 | autoTrim?: boolean; // to avoid the log eating up potentially endless memory 8 | maxLines?: number; // if autoTrim is true, this many most recent lines are saved 9 | tailNumLines?: number; // default number of lines tail gets 10 | logFilename?: string; // filename of log downloaded with downloadLog() 11 | maxDepth?: number; // max recursion depth for logged objects 12 | localStorageKey?: string; // localStorage key 13 | indent?: string; // string to use for indent (2 spaces) 14 | quoteStrings?: boolean; // whether or not to put quotes around strings 15 | } 16 | 17 | const debugoutDefaults: DebugoutOptions = { 18 | realTimeLoggingOn: true, 19 | useTimestamps: false, 20 | includeSessionMetadata: true, 21 | useLocalStorage: false, 22 | recordLogs: true, 23 | autoTrim: true, 24 | maxLines: 3000, 25 | tailNumLines: 25, 26 | maxDepth: 20, 27 | logFilename: 'debugout.txt', 28 | localStorageKey: 'debugout.js', 29 | indent: ' ', 30 | quoteStrings: true 31 | }; 32 | 33 | export interface DebugoutStorage { 34 | startTime: string; 35 | log: string; 36 | lastLog: string; 37 | } 38 | 39 | declare var window; 40 | 41 | export class Debugout { 42 | 43 | // options 44 | realTimeLoggingOn: boolean; 45 | includeSessionMetadata: boolean; 46 | useTimestamps: boolean; 47 | useLocalStorage: boolean; 48 | recordLogs: boolean; 49 | autoTrim: boolean; 50 | maxLines: number; 51 | logFilename: string; 52 | maxDepth: number; 53 | localStorageKey: string; 54 | indent = ' '; 55 | quoteStrings: boolean; 56 | 57 | tailNumLines = 25; 58 | startTime: Date; 59 | output = ''; // holds all logs 60 | 61 | version = () => '1.1.0'; 62 | indentsForDepth = (depth: number) => this.indent.repeat(Math.max(depth, 0)); 63 | 64 | // forwarded console methods not used by debugout 65 | /* tslint:disable:no-console */ 66 | trace = () => console.trace(); 67 | time = () => console.time(); 68 | timeEnd = () => console.timeEnd(); 69 | /* tslint:enable:no-console */ 70 | 71 | constructor(options?: DebugoutOptions) { 72 | // set options from defaults and passed options. 73 | const settings = { 74 | ...debugoutDefaults, 75 | ...options 76 | }; 77 | for (const prop in settings) { 78 | if (settings[prop] !== undefined) { 79 | this[prop] = settings[prop]; 80 | } 81 | } 82 | 83 | // START/RESUME LOG 84 | if (this.useLocalStorage && window && !!window.localStorage) { 85 | const stored = this.load(); 86 | if (stored) { 87 | this.output = stored.log; 88 | this.startTime = new Date(stored.startTime); 89 | const end = new Date(stored.lastLog); 90 | this.logMetadata(`Last session end: ${stored.lastLog}`); 91 | this.logMetadata(`Last ${this.formatSessionDuration(this.startTime, end)}`); 92 | this.startLog(); 93 | } else { 94 | this.startLog(); 95 | } 96 | } else { 97 | this.useLocalStorage = false; 98 | this.startLog(); 99 | } 100 | } 101 | 102 | private startLog(): void { 103 | this.startTime = new Date(); 104 | this.logMetadata(`Session started: ${this.formatDate(this.startTime)}`); 105 | } 106 | 107 | // records a log 108 | private recordLog(...args: unknown[]): void { 109 | // record log 110 | if (this.useTimestamps) { 111 | this.output += this.formatDate() + ' '; 112 | } 113 | this.output += args.map(obj => this.stringify(obj)).join(' '); 114 | this.output += '\n'; 115 | if (this.autoTrim) this.output = this.trimLog(this.maxLines); 116 | if (this.useLocalStorage) { 117 | const saveObject = { 118 | startTime: this.startTime, 119 | log: this.output, 120 | lastLog: new Date() 121 | }; 122 | window.localStorage.setItem(this.localStorageKey, JSON.stringify(saveObject)); 123 | } 124 | } 125 | 126 | private logMetadata(msg: string): void { 127 | if (this.includeSessionMetadata) this.output += `---- ${msg} ----\n`; 128 | } 129 | 130 | // USER METHODS 131 | 132 | log(...args: unknown[]): void { 133 | if (this.realTimeLoggingOn) console.log(...args); 134 | if (this.recordLogs) this.recordLog(...args); 135 | } 136 | info(...args: unknown[]): void { 137 | // tslint:disable-next-line:no-console 138 | if (this.realTimeLoggingOn) console.info(...args); 139 | if (this.recordLogs) { 140 | this.output += '[INFO] '; 141 | this.recordLog(...args); 142 | } 143 | } 144 | warn(...args: unknown[]): void { 145 | if (this.realTimeLoggingOn) console.warn(...args); 146 | if (this.recordLogs) { 147 | this.output += '[WARN] '; 148 | this.recordLog(...args); 149 | } 150 | } 151 | error(...args: unknown[]): void { 152 | if (this.realTimeLoggingOn) console.error(...args); 153 | if (this.recordLogs) { 154 | this.output += '[ERROR] '; 155 | this.recordLog(...args); 156 | } 157 | } 158 | debug(...args: unknown[]): void { 159 | if (this.realTimeLoggingOn) console.debug(...args); 160 | if (this.recordLogs) { 161 | this.output += '[DEBUG] '; 162 | this.recordLog(...args); 163 | } 164 | } 165 | 166 | getLog(): string { 167 | const retrievalTime = new Date(); 168 | // if recording is off, so dev knows why they don't have any logs 169 | if (!this.recordLogs) { 170 | this.info('Log recording is off'); 171 | } 172 | // if using local storage, get values 173 | if (this.useLocalStorage && window && window.localStorage) { 174 | const stored = this.load(); 175 | if (stored) { 176 | this.startTime = new Date(stored.startTime); 177 | this.output = stored.log; 178 | } 179 | } 180 | if (this.includeSessionMetadata) { 181 | return this.output + `---- ${this.formatSessionDuration(this.startTime, retrievalTime)} ----\n`; 182 | } 183 | return this.output; 184 | } 185 | 186 | // clears the log 187 | clear(): void { 188 | this.output = ''; 189 | this.logMetadata(`Session started: ${this.formatDate(this.startTime)}`); 190 | this.logMetadata('Log cleared ' + this.formatDate()); 191 | if (this.useLocalStorage) this.save(); 192 | } 193 | 194 | // gets last X number of lines 195 | tail(numLines?: number): string { 196 | const lines = numLines || this.tailNumLines; 197 | return this.trimLog(lines); 198 | } 199 | 200 | // find occurences of your search term in the log 201 | search(term: string): string { 202 | const rgx = new RegExp(term, 'ig'); 203 | const lines = this.output.split('\n'); 204 | const matched = []; 205 | // can't use a simple filter & map here because we need to add the line number 206 | for (let i = 0; i < lines.length; i++) { 207 | const addr = `[${i}] `; 208 | if (lines[i].match(rgx)) { 209 | matched.push(addr + lines[i].trim()); 210 | } 211 | } 212 | let result = matched.join('\n'); 213 | if (!result.length) result = `Nothing found for "${term}".`; 214 | return result; 215 | } 216 | 217 | // retrieve a section of the log. Works the same as js slice 218 | slice(...args: number[]): string { 219 | return this.output.split('\n').slice(...args).join('\n'); 220 | } 221 | 222 | // downloads the log - for browser use 223 | downloadLog(): void { 224 | if (!!window) { 225 | const logFile = this.getLog(); 226 | const blob = new Blob([logFile], { type: 'data:text/plain;charset=utf-8' }); 227 | const a = document.createElement('a'); 228 | a.href = window.URL.createObjectURL(blob); 229 | a.target = '_blank'; 230 | a.download = this.logFilename; 231 | document.body.appendChild(a); 232 | a.click(); 233 | document.body.removeChild(a); 234 | window.URL.revokeObjectURL(a.href); 235 | } else { 236 | console.error('downloadLog only works in the browser'); 237 | } 238 | 239 | } 240 | 241 | // METHODS FOR CONSTRUCTING THE LOG 242 | 243 | private save(): void { 244 | const saveObject = { 245 | startTime: this.startTime, 246 | log: this.output, 247 | lastLog: new Date() 248 | }; 249 | window.localStorage.setItem(this.localStorageKey, JSON.stringify(saveObject)); 250 | } 251 | 252 | private load(): DebugoutStorage { 253 | const saved = window.localStorage.getItem(this.localStorageKey); 254 | if (saved) { 255 | return JSON.parse(saved) as DebugoutStorage; 256 | } 257 | return null; 258 | } 259 | 260 | determineType(object: any): string { 261 | if (object === null) { 262 | return 'null'; 263 | } else if (object === undefined) { 264 | return 'undefined'; 265 | } else { 266 | let type = typeof object as string; 267 | if (type === 'object') { 268 | if (Array.isArray(object)) { 269 | type = 'Array'; 270 | } else { 271 | if (object instanceof Date) { 272 | type = 'Date'; 273 | } 274 | else if (object instanceof RegExp) { 275 | type = 'RegExp'; 276 | } 277 | else if (object instanceof Debugout) { 278 | type = 'Debugout'; 279 | } 280 | else { 281 | type = 'Object'; 282 | } 283 | } 284 | } 285 | return type; 286 | } 287 | } 288 | 289 | // recursively stringify object 290 | stringifyObject(obj: any, startingDepth = 0): string { 291 | // return JSON.stringify(obj, null, this.indent); // can't control depth/line-breaks/quotes 292 | let result = '{'; 293 | let depth = startingDepth; 294 | if (this.objectSize(obj) > 0) { 295 | result += '\n'; 296 | depth++; 297 | let i = 0; 298 | for (const prop in obj) { 299 | result += this.indentsForDepth(depth); 300 | result += prop + ': '; 301 | const subresult = this.stringify(obj[prop], depth); 302 | if (subresult) { 303 | result += subresult; 304 | } 305 | if (i < this.objectSize(obj) - 1) result += ','; 306 | result += '\n'; 307 | i++; 308 | } 309 | depth--; 310 | result += this.indentsForDepth(depth); 311 | } 312 | result += '}'; 313 | return result; 314 | } 315 | 316 | // recursively stringify array 317 | stringifyArray(arr: Array, startingDepth = 0): string { 318 | // return JSON.stringify(arr, null, this.indent); // can't control depth/line-breaks/quotes 319 | let result = '['; 320 | let depth = startingDepth; 321 | let lastLineNeedsNewLine = false; 322 | if (arr.length > 0) { 323 | depth++; 324 | for (let i = 0; i < arr.length; i++) { 325 | const subtype = this.determineType(arr[i]); 326 | let needsNewLine = false; 327 | if (subtype === 'Object' && this.objectSize(arr[i]) > 0) needsNewLine = true; 328 | if (subtype === 'Array' && arr[i].length > 0) needsNewLine = true; 329 | if (!lastLineNeedsNewLine && needsNewLine) result += '\n'; 330 | const subresult = this.stringify(arr[i], depth); 331 | if (subresult) { 332 | if (needsNewLine) result += this.indentsForDepth(depth); 333 | result += subresult; 334 | if (i < arr.length - 1) result += ', '; 335 | if (needsNewLine) result += '\n'; 336 | } 337 | lastLineNeedsNewLine = needsNewLine; 338 | } 339 | depth--; 340 | } 341 | result += ']'; 342 | return result; 343 | } 344 | 345 | // pretty-printing functions is a lib unto itself - this simply prints with indents 346 | stringifyFunction(fn: any, startingDepth = 0): string { 347 | let depth = startingDepth; 348 | return String(fn).split('\n').map(line => { 349 | if (line.match(/\}/)) depth--; 350 | const val = this.indentsForDepth(depth) + line.trim(); 351 | if (line.match(/\{/)) depth++; 352 | return val; 353 | }).join('\n'); 354 | } 355 | 356 | // stringify any data 357 | stringify(obj: any, depth = 0): string { 358 | if (depth >= this.maxDepth) { 359 | return '... (max-depth reached)'; 360 | } 361 | const type = this.determineType(obj); 362 | switch (type) { 363 | case 'Object': 364 | return this.stringifyObject(obj, depth); 365 | case 'Array': 366 | return this.stringifyArray(obj, depth); 367 | case 'function': 368 | return this.stringifyFunction(obj, depth); 369 | case 'RegExp': 370 | return '/' + obj.source + '/' + obj.flags; 371 | case 'Date': 372 | case 'string': 373 | return (this.quoteStrings) ? `"${obj}"` : obj + ''; 374 | case 'boolean': 375 | return (obj) ? 'true' : 'false'; 376 | case 'number': 377 | return obj + ''; 378 | case 'null': 379 | case 'undefined': 380 | return type; 381 | case 'Debugout': 382 | return '... (Debugout)'; // prevent endless loop 383 | default: 384 | return '?'; 385 | } 386 | } 387 | 388 | trimLog(maxLines: number): string { 389 | let lines = this.output.split('\n'); 390 | lines.pop(); 391 | if (lines.length > maxLines) { 392 | lines = lines.slice(lines.length - maxLines); 393 | } 394 | return lines.join('\n') + '\n'; 395 | } 396 | 397 | // no type args: typescript doesn't think dates can be subtracted but they can 398 | private formatSessionDuration(startTime, endTime): string { 399 | let msec = endTime - startTime; 400 | const hh = Math.floor(msec / 1000 / 60 / 60); 401 | const hrs = ('0' + hh).slice(-2); 402 | msec -= hh * 1000 * 60 * 60; 403 | const mm = Math.floor(msec / 1000 / 60); 404 | const mins = ('0' + mm).slice(-2); 405 | msec -= mm * 1000 * 60; 406 | const ss = Math.floor(msec / 1000); 407 | const secs = ('0' + ss).slice(-2); 408 | msec -= ss * 1000; 409 | return 'Session duration: ' + hrs + ':' + mins + ':' + secs; 410 | } 411 | 412 | formatDate(ts = new Date()): string { 413 | return `[${ts.toISOString()}]`; 414 | } 415 | 416 | objectSize(obj: any): number { 417 | let size = 0; 418 | for (const key in obj) { 419 | if (obj.hasOwnProperty(key)) size++; 420 | } 421 | return size; 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2017", "DOM"], 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "outDir": "dist", 8 | "target": "es5", 9 | "pretty": true, 10 | "esModuleInterop": true 11 | }, 12 | "compileOnSave": true, 13 | "include": ["src"], 14 | "exclude": [ 15 | "node_modules", 16 | "**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "align": { 5 | "options": [ 6 | "parameters", 7 | "statements" 8 | ] 9 | }, 10 | "forin": false, 11 | "array-type": false, 12 | "arrow-return-shorthand": true, 13 | "curly": [true, "ignore-same-line"], 14 | "deprecation": { 15 | "severity": "warning" 16 | }, 17 | "eofline": true, 18 | "import-blacklist": [ 19 | true, 20 | "rxjs/Rx" 21 | ], 22 | "import-spacing": true, 23 | "indent": { 24 | "options": [ 25 | "spaces" 26 | ] 27 | }, 28 | "max-classes-per-file": false, 29 | "max-line-length": [ 30 | true, 31 | 140 32 | ], 33 | "member-ordering": [ 34 | true, 35 | { 36 | "order": [ 37 | "static-field", 38 | "instance-field", 39 | "static-method", 40 | "instance-method" 41 | ] 42 | } 43 | ], 44 | "no-console": [ 45 | true, 46 | "info", 47 | "time", 48 | "timeEnd", 49 | "trace" 50 | ], 51 | "no-empty": false, 52 | "no-inferrable-types": [ 53 | true, 54 | "ignore-params" 55 | ], 56 | "no-non-null-assertion": true, 57 | "no-redundant-jsdoc": true, 58 | "no-switch-case-fall-through": true, 59 | "no-var-requires": false, 60 | "object-literal-key-quotes": [ 61 | true, 62 | "as-needed" 63 | ], 64 | "quotemark": [ 65 | true, 66 | "single" 67 | ], 68 | "semicolon": { 69 | "options": [ 70 | "always" 71 | ] 72 | }, 73 | "space-before-function-paren": { 74 | "options": { 75 | "anonymous": "never", 76 | "asyncArrow": "always", 77 | "constructor": "never", 78 | "method": "never", 79 | "named": "never" 80 | } 81 | }, 82 | "typedef": [ 83 | true, 84 | "call-signature" 85 | ], 86 | "typedef-whitespace": { 87 | "options": [ 88 | { 89 | "call-signature": "nospace", 90 | "index-signature": "nospace", 91 | "parameter": "nospace", 92 | "property-declaration": "nospace", 93 | "variable-declaration": "nospace" 94 | }, 95 | { 96 | "call-signature": "onespace", 97 | "index-signature": "onespace", 98 | "parameter": "onespace", 99 | "property-declaration": "onespace", 100 | "variable-declaration": "onespace" 101 | } 102 | ] 103 | }, 104 | "variable-name": { 105 | "options": [ 106 | "ban-keywords", 107 | "check-format", 108 | "allow-pascal-case" 109 | ] 110 | }, 111 | "whitespace": { 112 | "options": [ 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type", 118 | "check-typecast" 119 | ] 120 | } 121 | } 122 | } --------------------------------------------------------------------------------