debugwindow.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  1. // Copyright 2006 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Definition of the DebugWindow class. Please minimize
  16. * dependencies this file has on other closure classes as any dependency it
  17. * takes won't be able to use the logging infrastructure.
  18. *
  19. */
  20. goog.provide('goog.debug.DebugWindow');
  21. goog.require('goog.debug.HtmlFormatter');
  22. goog.require('goog.debug.LogManager');
  23. goog.require('goog.debug.Logger');
  24. goog.require('goog.dom.safe');
  25. goog.require('goog.html.SafeHtml');
  26. goog.require('goog.html.SafeStyleSheet');
  27. goog.require('goog.string.Const');
  28. goog.require('goog.structs.CircularBuffer');
  29. goog.require('goog.userAgent');
  30. /**
  31. * Provides a debug DebugWindow that is bound to the goog.debug.Logger.
  32. * It handles log messages and writes them to the DebugWindow. This doesn't
  33. * provide a lot of functionality that the old Gmail logging infrastructure
  34. * provided like saving debug logs for exporting to the server. Now that we
  35. * have an event-based logging infrastructure, we can encapsulate that
  36. * functionality in a separate class.
  37. *
  38. * @constructor
  39. * @param {string=} opt_identifier Identifier for this logging class.
  40. * @param {string=} opt_prefix Prefix prepended to messages.
  41. */
  42. goog.debug.DebugWindow = function(opt_identifier, opt_prefix) {
  43. /**
  44. * Identifier for this logging class
  45. * @protected {string}
  46. */
  47. this.identifier = opt_identifier || '';
  48. /**
  49. * Array used to buffer log output
  50. * @protected {!Array<!goog.html.SafeHtml>}
  51. */
  52. this.outputBuffer = [];
  53. /**
  54. * Optional prefix to be prepended to error strings
  55. * @private {string}
  56. */
  57. this.prefix_ = opt_prefix || '';
  58. /**
  59. * Buffer for saving the last 1000 messages
  60. * @private {!goog.structs.CircularBuffer}
  61. */
  62. this.savedMessages_ =
  63. new goog.structs.CircularBuffer(goog.debug.DebugWindow.MAX_SAVED);
  64. /**
  65. * Save the publish handler so it can be removed
  66. * @private {!Function}
  67. */
  68. this.publishHandler_ = goog.bind(this.addLogRecord, this);
  69. /**
  70. * Formatter for formatted output
  71. * @private {goog.debug.Formatter}
  72. */
  73. this.formatter_ = new goog.debug.HtmlFormatter(this.prefix_);
  74. /**
  75. * Loggers that we shouldn't output
  76. * @private {!Object}
  77. */
  78. this.filteredLoggers_ = {};
  79. // enable by default
  80. this.setCapturing(true);
  81. /**
  82. * Whether we are currently enabled. When the DebugWindow is enabled, it tries
  83. * to keep its window open. When it's disabled, it can still be capturing log
  84. * output if, but it won't try to write them to the DebugWindow window until
  85. * it's enabled.
  86. * @private {boolean}
  87. */
  88. this.enabled_ = goog.debug.DebugWindow.isEnabled(this.identifier);
  89. // timer to save the DebugWindow's window position in a cookie
  90. goog.global.setInterval(goog.bind(this.saveWindowPositionSize_, this), 7500);
  91. };
  92. /**
  93. * Max number of messages to be saved
  94. * @type {number}
  95. */
  96. goog.debug.DebugWindow.MAX_SAVED = 500;
  97. /**
  98. * How long to keep the cookies for in milliseconds
  99. * @type {number}
  100. */
  101. goog.debug.DebugWindow.COOKIE_TIME = 30 * 24 * 60 * 60 * 1000; // 30-days
  102. /**
  103. * HTML string printed when the debug window opens
  104. * @type {string}
  105. * @protected
  106. */
  107. goog.debug.DebugWindow.prototype.welcomeMessage = 'LOGGING';
  108. /**
  109. * Whether to force enable the window on a severe log.
  110. * @type {boolean}
  111. * @private
  112. */
  113. goog.debug.DebugWindow.prototype.enableOnSevere_ = false;
  114. /**
  115. * Reference to debug window
  116. * @type {Window}
  117. * @protected
  118. */
  119. goog.debug.DebugWindow.prototype.win = null;
  120. /**
  121. * In the process of opening the window
  122. * @type {boolean}
  123. * @private
  124. */
  125. goog.debug.DebugWindow.prototype.winOpening_ = false;
  126. /**
  127. * Whether we are currently capturing logger output.
  128. *
  129. * @type {boolean}
  130. * @private
  131. */
  132. goog.debug.DebugWindow.prototype.isCapturing_ = false;
  133. /**
  134. * Whether we already showed an alert that the DebugWindow was blocked.
  135. * @type {boolean}
  136. * @private
  137. */
  138. goog.debug.DebugWindow.showedBlockedAlert_ = false;
  139. /**
  140. * Reference to timeout used to buffer the output stream.
  141. * @type {?number}
  142. * @private
  143. */
  144. goog.debug.DebugWindow.prototype.bufferTimeout_ = null;
  145. /**
  146. * Timestamp for the last time the log was written to.
  147. * @protected {number}
  148. */
  149. goog.debug.DebugWindow.prototype.lastCall = goog.now();
  150. /**
  151. * Sets the welcome message shown when the window is first opened or reset.
  152. *
  153. * @param {string} msg An HTML string.
  154. */
  155. goog.debug.DebugWindow.prototype.setWelcomeMessage = function(msg) {
  156. this.welcomeMessage = msg;
  157. };
  158. /**
  159. * Initializes the debug window.
  160. */
  161. goog.debug.DebugWindow.prototype.init = function() {
  162. if (this.enabled_) {
  163. this.openWindow_();
  164. }
  165. };
  166. /**
  167. * Whether the DebugWindow is enabled. When the DebugWindow is enabled, it
  168. * tries to keep its window open and logs all messages to the window. When the
  169. * DebugWindow is disabled, it stops logging messages to its window.
  170. *
  171. * @return {boolean} Whether the DebugWindow is enabled.
  172. */
  173. goog.debug.DebugWindow.prototype.isEnabled = function() {
  174. return this.enabled_;
  175. };
  176. /**
  177. * Sets whether the DebugWindow is enabled. When the DebugWindow is enabled, it
  178. * tries to keep its window open and log all messages to the window. When the
  179. * DebugWindow is disabled, it stops logging messages to its window. The
  180. * DebugWindow also saves this state to a cookie so that it's persisted across
  181. * application refreshes.
  182. * @param {boolean} enable Whether the DebugWindow is enabled.
  183. */
  184. goog.debug.DebugWindow.prototype.setEnabled = function(enable) {
  185. this.enabled_ = enable;
  186. if (this.enabled_) {
  187. this.openWindow_();
  188. }
  189. this.setCookie_('enabled', enable ? '1' : '0');
  190. };
  191. /**
  192. * Sets whether the debug window should be force enabled when a severe log is
  193. * encountered.
  194. * @param {boolean} enableOnSevere Whether to enable on severe logs..
  195. */
  196. goog.debug.DebugWindow.prototype.setForceEnableOnSevere = function(
  197. enableOnSevere) {
  198. this.enableOnSevere_ = enableOnSevere;
  199. };
  200. /**
  201. * Whether we are currently capturing logger output.
  202. * @return {boolean} whether we are currently capturing logger output.
  203. */
  204. goog.debug.DebugWindow.prototype.isCapturing = function() {
  205. return this.isCapturing_;
  206. };
  207. /**
  208. * Sets whether we are currently capturing logger output.
  209. * @param {boolean} capturing Whether to capture logger output.
  210. */
  211. goog.debug.DebugWindow.prototype.setCapturing = function(capturing) {
  212. if (capturing == this.isCapturing_) {
  213. return;
  214. }
  215. this.isCapturing_ = capturing;
  216. // attach or detach handler from the root logger
  217. var rootLogger = goog.debug.LogManager.getRoot();
  218. if (capturing) {
  219. rootLogger.addHandler(this.publishHandler_);
  220. } else {
  221. rootLogger.removeHandler(this.publishHandler_);
  222. }
  223. };
  224. /**
  225. * Gets the formatter for outputting to the debug window. The default formatter
  226. * is an instance of goog.debug.HtmlFormatter
  227. * @return {goog.debug.Formatter} The formatter in use.
  228. */
  229. goog.debug.DebugWindow.prototype.getFormatter = function() {
  230. return this.formatter_;
  231. };
  232. /**
  233. * Sets the formatter for outputting to the debug window.
  234. * @param {goog.debug.Formatter} formatter The formatter to use.
  235. */
  236. goog.debug.DebugWindow.prototype.setFormatter = function(formatter) {
  237. this.formatter_ = formatter;
  238. };
  239. /**
  240. * Adds a separator to the debug window.
  241. */
  242. goog.debug.DebugWindow.prototype.addSeparator = function() {
  243. this.write_(goog.html.SafeHtml.create('hr'));
  244. };
  245. /**
  246. * @return {boolean} Whether there is an active window.
  247. */
  248. goog.debug.DebugWindow.prototype.hasActiveWindow = function() {
  249. return !!this.win && !this.win.closed;
  250. };
  251. /**
  252. * Clears the contents of the debug window
  253. * @protected
  254. */
  255. goog.debug.DebugWindow.prototype.clear = function() {
  256. this.savedMessages_.clear();
  257. if (this.hasActiveWindow()) {
  258. this.writeInitialDocument();
  259. }
  260. };
  261. /**
  262. * Adds a log record.
  263. * @param {goog.debug.LogRecord} logRecord the LogRecord.
  264. */
  265. goog.debug.DebugWindow.prototype.addLogRecord = function(logRecord) {
  266. if (this.filteredLoggers_[logRecord.getLoggerName()]) {
  267. return;
  268. }
  269. var html = this.formatter_.formatRecordAsHtml(logRecord);
  270. this.write_(html);
  271. if (this.enableOnSevere_ &&
  272. logRecord.getLevel().value >= goog.debug.Logger.Level.SEVERE.value) {
  273. this.setEnabled(true);
  274. }
  275. };
  276. /**
  277. * Writes a message to the log, possibly opening up the window if it's enabled,
  278. * or saving it if it's disabled.
  279. * @param {!goog.html.SafeHtml} html The HTML to write.
  280. * @private
  281. */
  282. goog.debug.DebugWindow.prototype.write_ = function(html) {
  283. // If the logger is enabled, open window and write html message to log
  284. // otherwise save it
  285. if (this.enabled_) {
  286. this.openWindow_();
  287. this.savedMessages_.add(html);
  288. this.writeToLog_(html);
  289. } else {
  290. this.savedMessages_.add(html);
  291. }
  292. };
  293. /**
  294. * Write to the buffer. If a message hasn't been sent for more than 750ms just
  295. * write, otherwise delay for a minimum of 250ms.
  296. * @param {!goog.html.SafeHtml} html HTML to post to the log.
  297. * @private
  298. */
  299. goog.debug.DebugWindow.prototype.writeToLog_ = function(html) {
  300. this.outputBuffer.push(html);
  301. goog.global.clearTimeout(this.bufferTimeout_);
  302. if (goog.now() - this.lastCall > 750) {
  303. this.writeBufferToLog();
  304. } else {
  305. this.bufferTimeout_ =
  306. goog.global.setTimeout(goog.bind(this.writeBufferToLog, this), 250);
  307. }
  308. };
  309. /**
  310. * Write to the log and maybe scroll into view.
  311. * @protected
  312. */
  313. goog.debug.DebugWindow.prototype.writeBufferToLog = function() {
  314. this.lastCall = goog.now();
  315. if (this.hasActiveWindow()) {
  316. var body = this.win.document.body;
  317. var scroll =
  318. body && body.scrollHeight - (body.scrollTop + body.clientHeight) <= 100;
  319. goog.dom.safe.documentWrite(
  320. this.win.document, goog.html.SafeHtml.concat(this.outputBuffer));
  321. this.outputBuffer.length = 0;
  322. if (scroll) {
  323. this.win.scrollTo(0, 1000000);
  324. }
  325. }
  326. };
  327. /**
  328. * Writes all saved messages to the DebugWindow.
  329. * @protected
  330. */
  331. goog.debug.DebugWindow.prototype.writeSavedMessages = function() {
  332. var messages = this.savedMessages_.getValues();
  333. for (var i = 0; i < messages.length; i++) {
  334. this.writeToLog_(messages[i]);
  335. }
  336. };
  337. /**
  338. * Opens the debug window if it is not already referenced
  339. * @private
  340. */
  341. goog.debug.DebugWindow.prototype.openWindow_ = function() {
  342. if (this.hasActiveWindow() || this.winOpening_) {
  343. return;
  344. }
  345. var winpos = this.getCookie_('dbg', '0,0,800,500').split(',');
  346. var x = Number(winpos[0]);
  347. var y = Number(winpos[1]);
  348. var w = Number(winpos[2]);
  349. var h = Number(winpos[3]);
  350. this.winOpening_ = true;
  351. this.win = window.open(
  352. '', this.getWindowName_(), 'width=' + w + ',height=' + h +
  353. ',toolbar=no,resizable=yes,' +
  354. 'scrollbars=yes,left=' + x + ',top=' + y + ',status=no,screenx=' + x +
  355. ',screeny=' + y);
  356. if (!this.win) {
  357. if (!goog.debug.DebugWindow.showedBlockedAlert_) {
  358. // only show this once
  359. alert('Logger popup was blocked');
  360. goog.debug.DebugWindow.showedBlockedAlert_ = true;
  361. }
  362. }
  363. this.winOpening_ = false;
  364. if (this.win) {
  365. this.writeInitialDocument();
  366. }
  367. };
  368. /**
  369. * Gets a valid window name for the debug window. Replaces invalid characters in
  370. * IE.
  371. * @return {string} Valid window name.
  372. * @private
  373. */
  374. goog.debug.DebugWindow.prototype.getWindowName_ = function() {
  375. return goog.userAgent.IE ? this.identifier.replace(/[\s\-\.\,]/g, '_') :
  376. this.identifier;
  377. };
  378. /**
  379. * @return {!goog.html.SafeStyleSheet} The stylesheet, for inclusion in the
  380. * initial HTML.
  381. */
  382. goog.debug.DebugWindow.prototype.getStyleRules = function() {
  383. return goog.html.SafeStyleSheet.fromConstant(
  384. goog.string.Const.from(
  385. '*{font:normal 14px monospace;}' +
  386. '.dbg-sev{color:#F00}' +
  387. '.dbg-w{color:#E92}' +
  388. '.dbg-sh{background-color:#fd4;font-weight:bold;color:#000}' +
  389. '.dbg-i{color:#666}' +
  390. '.dbg-f{color:#999}' +
  391. '.dbg-ev{color:#0A0}' +
  392. '.dbg-m{color:#990}'));
  393. };
  394. /**
  395. * Writes the initial HTML of the debug window.
  396. * @protected
  397. */
  398. goog.debug.DebugWindow.prototype.writeInitialDocument = function() {
  399. if (!this.hasActiveWindow()) {
  400. return;
  401. }
  402. this.win.document.open();
  403. var div = goog.html.SafeHtml.create(
  404. 'div', {
  405. 'class': 'dbg-ev',
  406. 'style': goog.string.Const.from('text-align:center;')
  407. },
  408. goog.html.SafeHtml.concat(
  409. this.welcomeMessage, goog.html.SafeHtml.BR,
  410. goog.html.SafeHtml.create(
  411. 'small', {}, 'Logger: ' + this.identifier)));
  412. var html = goog.html.SafeHtml.concat(
  413. goog.html.SafeHtml.createStyle(this.getStyleRules()),
  414. goog.html.SafeHtml.create('hr'), div, goog.html.SafeHtml.create('hr'));
  415. this.writeToLog_(html);
  416. this.writeSavedMessages();
  417. };
  418. /**
  419. * Save persistent data (using cookies) for 1 month (cookie specific to this
  420. * logger object).
  421. * @param {string} key Data name.
  422. * @param {string} value Data value.
  423. * @private
  424. */
  425. goog.debug.DebugWindow.prototype.setCookie_ = function(key, value) {
  426. var fullKey = goog.debug.DebugWindow.getCookieKey_(this.identifier, key);
  427. document.cookie = fullKey + '=' + encodeURIComponent(value) +
  428. ';path=/;expires=' +
  429. (new Date(goog.now() + goog.debug.DebugWindow.COOKIE_TIME)).toUTCString();
  430. };
  431. /**
  432. * Retrieve data (using cookies).
  433. * @param {string} key Data name.
  434. * @param {string=} opt_default Optional default value if cookie doesn't exist.
  435. * @return {string} Cookie value.
  436. * @private
  437. */
  438. goog.debug.DebugWindow.prototype.getCookie_ = function(key, opt_default) {
  439. return goog.debug.DebugWindow.getCookieValue_(
  440. this.identifier, key, opt_default);
  441. };
  442. /**
  443. * Creates a valid cookie key name which is scoped to the given identifier.
  444. * Substitutes all occurrences of invalid cookie name characters (whitespace,
  445. * ';', and '=') with '_', which is a valid and readable alternative.
  446. * @see goog.net.Cookies#isValidName
  447. * @see <a href="http://tools.ietf.org/html/rfc2109">RFC 2109</a>
  448. * @param {string} identifier Identifier for logging class.
  449. * @param {string} key Data name.
  450. * @return {string} Cookie key name.
  451. * @private
  452. */
  453. goog.debug.DebugWindow.getCookieKey_ = function(identifier, key) {
  454. var fullKey = key + identifier;
  455. return fullKey.replace(/[;=\s]/g, '_');
  456. };
  457. /**
  458. * Retrieve data (using cookies).
  459. * @param {string} identifier Identifier for logging class.
  460. * @param {string} key Data name.
  461. * @param {string=} opt_default Optional default value if cookie doesn't exist.
  462. * @return {string} Cookie value.
  463. * @private
  464. */
  465. goog.debug.DebugWindow.getCookieValue_ = function(
  466. identifier, key, opt_default) {
  467. var fullKey = goog.debug.DebugWindow.getCookieKey_(identifier, key);
  468. var cookie = String(document.cookie);
  469. var start = cookie.indexOf(fullKey + '=');
  470. if (start != -1) {
  471. var end = cookie.indexOf(';', start);
  472. return decodeURIComponent(
  473. cookie.substring(
  474. start + fullKey.length + 1, end == -1 ? cookie.length : end));
  475. } else {
  476. return opt_default || '';
  477. }
  478. };
  479. /**
  480. * @param {string} identifier Identifier for logging class.
  481. * @return {boolean} Whether the DebugWindow is enabled.
  482. */
  483. goog.debug.DebugWindow.isEnabled = function(identifier) {
  484. return goog.debug.DebugWindow.getCookieValue_(identifier, 'enabled') == '1';
  485. };
  486. /**
  487. * Saves the window position size to a cookie
  488. * @private
  489. */
  490. goog.debug.DebugWindow.prototype.saveWindowPositionSize_ = function() {
  491. if (!this.hasActiveWindow()) {
  492. return;
  493. }
  494. var x = this.win.screenX || this.win.screenLeft || 0;
  495. var y = this.win.screenY || this.win.screenTop || 0;
  496. var w = this.win.outerWidth || 800;
  497. var h = this.win.outerHeight || 500;
  498. this.setCookie_('dbg', x + ',' + y + ',' + w + ',' + h);
  499. };
  500. /**
  501. * Adds a logger name to be filtered.
  502. * @param {string} loggerName the logger name to add.
  503. */
  504. goog.debug.DebugWindow.prototype.addFilter = function(loggerName) {
  505. this.filteredLoggers_[loggerName] = 1;
  506. };
  507. /**
  508. * Removes a logger name to be filtered.
  509. * @param {string} loggerName the logger name to remove.
  510. */
  511. goog.debug.DebugWindow.prototype.removeFilter = function(loggerName) {
  512. delete this.filteredLoggers_[loggerName];
  513. };
  514. /**
  515. * Modify the size of the circular buffer. Allows the log to retain more
  516. * information while the window is closed.
  517. * @param {number} size New size of the circular buffer.
  518. */
  519. goog.debug.DebugWindow.prototype.resetBufferWithNewSize = function(size) {
  520. if (size > 0 && size < 50000) {
  521. this.clear();
  522. this.savedMessages_ = new goog.structs.CircularBuffer(size);
  523. }
  524. };