// Copyright 2006 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @fileoverview Definition of the DebugWindow class. Please minimize * dependencies this file has on other closure classes as any dependency it * takes won't be able to use the logging infrastructure. * */ goog.provide('goog.debug.DebugWindow'); goog.require('goog.debug.HtmlFormatter'); goog.require('goog.debug.LogManager'); goog.require('goog.debug.Logger'); goog.require('goog.dom.safe'); goog.require('goog.html.SafeHtml'); goog.require('goog.html.SafeStyleSheet'); goog.require('goog.string.Const'); goog.require('goog.structs.CircularBuffer'); goog.require('goog.userAgent'); /** * Provides a debug DebugWindow that is bound to the goog.debug.Logger. * It handles log messages and writes them to the DebugWindow. This doesn't * provide a lot of functionality that the old Gmail logging infrastructure * provided like saving debug logs for exporting to the server. Now that we * have an event-based logging infrastructure, we can encapsulate that * functionality in a separate class. * * @constructor * @param {string=} opt_identifier Identifier for this logging class. * @param {string=} opt_prefix Prefix prepended to messages. */ goog.debug.DebugWindow = function(opt_identifier, opt_prefix) { /** * Identifier for this logging class * @protected {string} */ this.identifier = opt_identifier || ''; /** * Array used to buffer log output * @protected {!Array} */ this.outputBuffer = []; /** * Optional prefix to be prepended to error strings * @private {string} */ this.prefix_ = opt_prefix || ''; /** * Buffer for saving the last 1000 messages * @private {!goog.structs.CircularBuffer} */ this.savedMessages_ = new goog.structs.CircularBuffer(goog.debug.DebugWindow.MAX_SAVED); /** * Save the publish handler so it can be removed * @private {!Function} */ this.publishHandler_ = goog.bind(this.addLogRecord, this); /** * Formatter for formatted output * @private {goog.debug.Formatter} */ this.formatter_ = new goog.debug.HtmlFormatter(this.prefix_); /** * Loggers that we shouldn't output * @private {!Object} */ this.filteredLoggers_ = {}; // enable by default this.setCapturing(true); /** * Whether we are currently enabled. When the DebugWindow is enabled, it tries * to keep its window open. When it's disabled, it can still be capturing log * output if, but it won't try to write them to the DebugWindow window until * it's enabled. * @private {boolean} */ this.enabled_ = goog.debug.DebugWindow.isEnabled(this.identifier); // timer to save the DebugWindow's window position in a cookie goog.global.setInterval(goog.bind(this.saveWindowPositionSize_, this), 7500); }; /** * Max number of messages to be saved * @type {number} */ goog.debug.DebugWindow.MAX_SAVED = 500; /** * How long to keep the cookies for in milliseconds * @type {number} */ goog.debug.DebugWindow.COOKIE_TIME = 30 * 24 * 60 * 60 * 1000; // 30-days /** * HTML string printed when the debug window opens * @type {string} * @protected */ goog.debug.DebugWindow.prototype.welcomeMessage = 'LOGGING'; /** * Whether to force enable the window on a severe log. * @type {boolean} * @private */ goog.debug.DebugWindow.prototype.enableOnSevere_ = false; /** * Reference to debug window * @type {Window} * @protected */ goog.debug.DebugWindow.prototype.win = null; /** * In the process of opening the window * @type {boolean} * @private */ goog.debug.DebugWindow.prototype.winOpening_ = false; /** * Whether we are currently capturing logger output. * * @type {boolean} * @private */ goog.debug.DebugWindow.prototype.isCapturing_ = false; /** * Whether we already showed an alert that the DebugWindow was blocked. * @type {boolean} * @private */ goog.debug.DebugWindow.showedBlockedAlert_ = false; /** * Reference to timeout used to buffer the output stream. * @type {?number} * @private */ goog.debug.DebugWindow.prototype.bufferTimeout_ = null; /** * Timestamp for the last time the log was written to. * @protected {number} */ goog.debug.DebugWindow.prototype.lastCall = goog.now(); /** * Sets the welcome message shown when the window is first opened or reset. * * @param {string} msg An HTML string. */ goog.debug.DebugWindow.prototype.setWelcomeMessage = function(msg) { this.welcomeMessage = msg; }; /** * Initializes the debug window. */ goog.debug.DebugWindow.prototype.init = function() { if (this.enabled_) { this.openWindow_(); } }; /** * Whether the DebugWindow is enabled. When the DebugWindow is enabled, it * tries to keep its window open and logs all messages to the window. When the * DebugWindow is disabled, it stops logging messages to its window. * * @return {boolean} Whether the DebugWindow is enabled. */ goog.debug.DebugWindow.prototype.isEnabled = function() { return this.enabled_; }; /** * Sets whether the DebugWindow is enabled. When the DebugWindow is enabled, it * tries to keep its window open and log all messages to the window. When the * DebugWindow is disabled, it stops logging messages to its window. The * DebugWindow also saves this state to a cookie so that it's persisted across * application refreshes. * @param {boolean} enable Whether the DebugWindow is enabled. */ goog.debug.DebugWindow.prototype.setEnabled = function(enable) { this.enabled_ = enable; if (this.enabled_) { this.openWindow_(); } this.setCookie_('enabled', enable ? '1' : '0'); }; /** * Sets whether the debug window should be force enabled when a severe log is * encountered. * @param {boolean} enableOnSevere Whether to enable on severe logs.. */ goog.debug.DebugWindow.prototype.setForceEnableOnSevere = function( enableOnSevere) { this.enableOnSevere_ = enableOnSevere; }; /** * Whether we are currently capturing logger output. * @return {boolean} whether we are currently capturing logger output. */ goog.debug.DebugWindow.prototype.isCapturing = function() { return this.isCapturing_; }; /** * Sets whether we are currently capturing logger output. * @param {boolean} capturing Whether to capture logger output. */ goog.debug.DebugWindow.prototype.setCapturing = function(capturing) { if (capturing == this.isCapturing_) { return; } this.isCapturing_ = capturing; // attach or detach handler from the root logger var rootLogger = goog.debug.LogManager.getRoot(); if (capturing) { rootLogger.addHandler(this.publishHandler_); } else { rootLogger.removeHandler(this.publishHandler_); } }; /** * Gets the formatter for outputting to the debug window. The default formatter * is an instance of goog.debug.HtmlFormatter * @return {goog.debug.Formatter} The formatter in use. */ goog.debug.DebugWindow.prototype.getFormatter = function() { return this.formatter_; }; /** * Sets the formatter for outputting to the debug window. * @param {goog.debug.Formatter} formatter The formatter to use. */ goog.debug.DebugWindow.prototype.setFormatter = function(formatter) { this.formatter_ = formatter; }; /** * Adds a separator to the debug window. */ goog.debug.DebugWindow.prototype.addSeparator = function() { this.write_(goog.html.SafeHtml.create('hr')); }; /** * @return {boolean} Whether there is an active window. */ goog.debug.DebugWindow.prototype.hasActiveWindow = function() { return !!this.win && !this.win.closed; }; /** * Clears the contents of the debug window * @protected */ goog.debug.DebugWindow.prototype.clear = function() { this.savedMessages_.clear(); if (this.hasActiveWindow()) { this.writeInitialDocument(); } }; /** * Adds a log record. * @param {goog.debug.LogRecord} logRecord the LogRecord. */ goog.debug.DebugWindow.prototype.addLogRecord = function(logRecord) { if (this.filteredLoggers_[logRecord.getLoggerName()]) { return; } var html = this.formatter_.formatRecordAsHtml(logRecord); this.write_(html); if (this.enableOnSevere_ && logRecord.getLevel().value >= goog.debug.Logger.Level.SEVERE.value) { this.setEnabled(true); } }; /** * Writes a message to the log, possibly opening up the window if it's enabled, * or saving it if it's disabled. * @param {!goog.html.SafeHtml} html The HTML to write. * @private */ goog.debug.DebugWindow.prototype.write_ = function(html) { // If the logger is enabled, open window and write html message to log // otherwise save it if (this.enabled_) { this.openWindow_(); this.savedMessages_.add(html); this.writeToLog_(html); } else { this.savedMessages_.add(html); } }; /** * Write to the buffer. If a message hasn't been sent for more than 750ms just * write, otherwise delay for a minimum of 250ms. * @param {!goog.html.SafeHtml} html HTML to post to the log. * @private */ goog.debug.DebugWindow.prototype.writeToLog_ = function(html) { this.outputBuffer.push(html); goog.global.clearTimeout(this.bufferTimeout_); if (goog.now() - this.lastCall > 750) { this.writeBufferToLog(); } else { this.bufferTimeout_ = goog.global.setTimeout(goog.bind(this.writeBufferToLog, this), 250); } }; /** * Write to the log and maybe scroll into view. * @protected */ goog.debug.DebugWindow.prototype.writeBufferToLog = function() { this.lastCall = goog.now(); if (this.hasActiveWindow()) { var body = this.win.document.body; var scroll = body && body.scrollHeight - (body.scrollTop + body.clientHeight) <= 100; goog.dom.safe.documentWrite( this.win.document, goog.html.SafeHtml.concat(this.outputBuffer)); this.outputBuffer.length = 0; if (scroll) { this.win.scrollTo(0, 1000000); } } }; /** * Writes all saved messages to the DebugWindow. * @protected */ goog.debug.DebugWindow.prototype.writeSavedMessages = function() { var messages = this.savedMessages_.getValues(); for (var i = 0; i < messages.length; i++) { this.writeToLog_(messages[i]); } }; /** * Opens the debug window if it is not already referenced * @private */ goog.debug.DebugWindow.prototype.openWindow_ = function() { if (this.hasActiveWindow() || this.winOpening_) { return; } var winpos = this.getCookie_('dbg', '0,0,800,500').split(','); var x = Number(winpos[0]); var y = Number(winpos[1]); var w = Number(winpos[2]); var h = Number(winpos[3]); this.winOpening_ = true; this.win = window.open( '', this.getWindowName_(), 'width=' + w + ',height=' + h + ',toolbar=no,resizable=yes,' + 'scrollbars=yes,left=' + x + ',top=' + y + ',status=no,screenx=' + x + ',screeny=' + y); if (!this.win) { if (!goog.debug.DebugWindow.showedBlockedAlert_) { // only show this once alert('Logger popup was blocked'); goog.debug.DebugWindow.showedBlockedAlert_ = true; } } this.winOpening_ = false; if (this.win) { this.writeInitialDocument(); } }; /** * Gets a valid window name for the debug window. Replaces invalid characters in * IE. * @return {string} Valid window name. * @private */ goog.debug.DebugWindow.prototype.getWindowName_ = function() { return goog.userAgent.IE ? this.identifier.replace(/[\s\-\.\,]/g, '_') : this.identifier; }; /** * @return {!goog.html.SafeStyleSheet} The stylesheet, for inclusion in the * initial HTML. */ goog.debug.DebugWindow.prototype.getStyleRules = function() { return goog.html.SafeStyleSheet.fromConstant( goog.string.Const.from( '*{font:normal 14px monospace;}' + '.dbg-sev{color:#F00}' + '.dbg-w{color:#E92}' + '.dbg-sh{background-color:#fd4;font-weight:bold;color:#000}' + '.dbg-i{color:#666}' + '.dbg-f{color:#999}' + '.dbg-ev{color:#0A0}' + '.dbg-m{color:#990}')); }; /** * Writes the initial HTML of the debug window. * @protected */ goog.debug.DebugWindow.prototype.writeInitialDocument = function() { if (!this.hasActiveWindow()) { return; } this.win.document.open(); var div = goog.html.SafeHtml.create( 'div', { 'class': 'dbg-ev', 'style': goog.string.Const.from('text-align:center;') }, goog.html.SafeHtml.concat( this.welcomeMessage, goog.html.SafeHtml.BR, goog.html.SafeHtml.create( 'small', {}, 'Logger: ' + this.identifier))); var html = goog.html.SafeHtml.concat( goog.html.SafeHtml.createStyle(this.getStyleRules()), goog.html.SafeHtml.create('hr'), div, goog.html.SafeHtml.create('hr')); this.writeToLog_(html); this.writeSavedMessages(); }; /** * Save persistent data (using cookies) for 1 month (cookie specific to this * logger object). * @param {string} key Data name. * @param {string} value Data value. * @private */ goog.debug.DebugWindow.prototype.setCookie_ = function(key, value) { var fullKey = goog.debug.DebugWindow.getCookieKey_(this.identifier, key); document.cookie = fullKey + '=' + encodeURIComponent(value) + ';path=/;expires=' + (new Date(goog.now() + goog.debug.DebugWindow.COOKIE_TIME)).toUTCString(); }; /** * Retrieve data (using cookies). * @param {string} key Data name. * @param {string=} opt_default Optional default value if cookie doesn't exist. * @return {string} Cookie value. * @private */ goog.debug.DebugWindow.prototype.getCookie_ = function(key, opt_default) { return goog.debug.DebugWindow.getCookieValue_( this.identifier, key, opt_default); }; /** * Creates a valid cookie key name which is scoped to the given identifier. * Substitutes all occurrences of invalid cookie name characters (whitespace, * ';', and '=') with '_', which is a valid and readable alternative. * @see goog.net.Cookies#isValidName * @see RFC 2109 * @param {string} identifier Identifier for logging class. * @param {string} key Data name. * @return {string} Cookie key name. * @private */ goog.debug.DebugWindow.getCookieKey_ = function(identifier, key) { var fullKey = key + identifier; return fullKey.replace(/[;=\s]/g, '_'); }; /** * Retrieve data (using cookies). * @param {string} identifier Identifier for logging class. * @param {string} key Data name. * @param {string=} opt_default Optional default value if cookie doesn't exist. * @return {string} Cookie value. * @private */ goog.debug.DebugWindow.getCookieValue_ = function( identifier, key, opt_default) { var fullKey = goog.debug.DebugWindow.getCookieKey_(identifier, key); var cookie = String(document.cookie); var start = cookie.indexOf(fullKey + '='); if (start != -1) { var end = cookie.indexOf(';', start); return decodeURIComponent( cookie.substring( start + fullKey.length + 1, end == -1 ? cookie.length : end)); } else { return opt_default || ''; } }; /** * @param {string} identifier Identifier for logging class. * @return {boolean} Whether the DebugWindow is enabled. */ goog.debug.DebugWindow.isEnabled = function(identifier) { return goog.debug.DebugWindow.getCookieValue_(identifier, 'enabled') == '1'; }; /** * Saves the window position size to a cookie * @private */ goog.debug.DebugWindow.prototype.saveWindowPositionSize_ = function() { if (!this.hasActiveWindow()) { return; } var x = this.win.screenX || this.win.screenLeft || 0; var y = this.win.screenY || this.win.screenTop || 0; var w = this.win.outerWidth || 800; var h = this.win.outerHeight || 500; this.setCookie_('dbg', x + ',' + y + ',' + w + ',' + h); }; /** * Adds a logger name to be filtered. * @param {string} loggerName the logger name to add. */ goog.debug.DebugWindow.prototype.addFilter = function(loggerName) { this.filteredLoggers_[loggerName] = 1; }; /** * Removes a logger name to be filtered. * @param {string} loggerName the logger name to remove. */ goog.debug.DebugWindow.prototype.removeFilter = function(loggerName) { delete this.filteredLoggers_[loggerName]; }; /** * Modify the size of the circular buffer. Allows the log to retain more * information while the window is closed. * @param {number} size New size of the circular buffer. */ goog.debug.DebugWindow.prototype.resetBufferWithNewSize = function(size) { if (size > 0 && size < 50000) { this.clear(); this.savedMessages_ = new goog.structs.CircularBuffer(size); } };