// Copyright 2009 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 ErrorReporter class, which creates an error
 * handler that reports any errors raised to a URL.
 *
 */

goog.provide('goog.debug.ErrorReporter');
goog.provide('goog.debug.ErrorReporter.ExceptionEvent');

goog.require('goog.asserts');
goog.require('goog.debug');
goog.require('goog.debug.ErrorHandler');
goog.require('goog.debug.entryPointRegistry');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.EventTarget');
goog.require('goog.log');
goog.require('goog.net.XhrIo');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.uri.utils');
goog.require('goog.userAgent');



/**
 * Constructs an error reporter. Internal Use Only. To install an error
 * reporter see the {@see #install} method below.
 *
 * @param {string} handlerUrl The URL to which all errors will be reported.
 * @param {function(!Error, !Object<string, string>)=}
 *     opt_contextProvider When a report is to be sent to the server,
 *     this method will be called, and given an opportunity to modify the
 *     context object before submission to the server.
 * @param {boolean=} opt_noAutoProtect Whether to automatically add handlers for
 *     onerror and to protect entry points.  If apps have other error reporting
 *     facilities, it may make sense for them to set these up themselves and use
 *     the ErrorReporter just for transmission of reports.
 * @constructor
 * @extends {goog.events.EventTarget}
 */
goog.debug.ErrorReporter = function(
    handlerUrl, opt_contextProvider, opt_noAutoProtect) {
  goog.debug.ErrorReporter.base(this, 'constructor');

  /**
   * Context provider, if one was provided.
   * @type {?function(!Error, !Object<string, string>)}
   * @private
   */
  this.contextProvider_ = opt_contextProvider || null;

  /**
   * The string prefix of any optional context parameters logged with the error.
   * @private {string}
   */
  this.contextPrefix_ = 'context.';

  /**
   * The number of bytes after which the ErrorReporter truncates the POST body.
   * If null, the ErrorReporter won't truncate the body.
   * @private {?number}
   */
  this.truncationLimit_ = null;

  /**
   * Additional arguments to append to URL before sending XHR.
   * @private {!Object<string,string>}
   */
  this.additionalArguments_ = {};

  /**
   * XHR sender.
   * @type {function(string, string, string, (Object|goog.structs.Map)=)}
   * @private
   */
  this.xhrSender_ = goog.debug.ErrorReporter.defaultXhrSender;

  /**
   * The URL at which all errors caught by this handler will be logged.
   *
   * @type {string}
   * @private
   */
  this.handlerUrl_ = handlerUrl;

  if (goog.debug.ErrorReporter.ALLOW_AUTO_PROTECT) {
    if (!opt_noAutoProtect) {
      /**
       * The internal error handler used to catch all errors.
       *
       * @private {goog.debug.ErrorHandler}
       */
      this.errorHandler_ = null;

      this.setup_();
    }
  } else if (!opt_noAutoProtect) {
    goog.asserts.fail(
        'opt_noAutoProtect cannot be false while ' +
        'goog.debug.ErrorReporter.ALLOW_AUTO_PROTECT is false.  Setting ' +
        'ALLOW_AUTO_PROTECT to false removes the necessary auto-protect code ' +
        'in compiled/optimized mode.');
  }
};
goog.inherits(goog.debug.ErrorReporter, goog.events.EventTarget);


/**
 * @define {boolean} If true, the code that provides additional entry point
 *     protection and setup is exposed in this file.  Set to false to avoid
 *     bringing in a lot of code from ErrorHandler and entryPointRegistry in
 *     compiled mode.
 */
goog.define('goog.debug.ErrorReporter.ALLOW_AUTO_PROTECT', true);



/**
 * Event broadcast when an exception is logged.
 * @param {Error} error The exception that was was reported.
 * @param {!Object<string, string>} context The context values sent to the
 *     server alongside this error.
 * @constructor
 * @extends {goog.events.Event}
 * @final
 */
goog.debug.ErrorReporter.ExceptionEvent = function(error, context) {
  goog.events.Event.call(this, goog.debug.ErrorReporter.ExceptionEvent.TYPE);

  /**
   * The error that was reported.
   * @type {Error}
   */
  this.error = error;

  /**
   * Context values sent to the server alongside this report.
   * @type {!Object<string, string>}
   */
  this.context = context;
};
goog.inherits(goog.debug.ErrorReporter.ExceptionEvent, goog.events.Event);


/**
 * Event type for notifying of a logged exception.
 * @type {string}
 */
goog.debug.ErrorReporter.ExceptionEvent.TYPE =
    goog.events.getUniqueId('exception');


/**
 * Extra headers for the error-reporting XHR.
 * @type {Object|goog.structs.Map|undefined}
 * @private
 */
goog.debug.ErrorReporter.prototype.extraHeaders_;


/**
 * Logging object.
 *
 * @type {goog.log.Logger}
 * @private
 */
goog.debug.ErrorReporter.logger_ =
    goog.log.getLogger('goog.debug.ErrorReporter');


/**
 * Installs an error reporter to catch all JavaScript errors raised.
 *
 * @param {string} loggingUrl The URL to which the errors caught will be
 *     reported.
 * @param {function(!Error, !Object<string, string>)=}
 *     opt_contextProvider When a report is to be sent to the server,
 *     this method will be called, and given an opportunity to modify the
 *     context object before submission to the server.
 * @param {boolean=} opt_noAutoProtect Whether to automatically add handlers for
 *     onerror and to protect entry points.  If apps have other error reporting
 *     facilities, it may make sense for them to set these up themselves and use
 *     the ErrorReporter just for transmission of reports.
 * @return {!goog.debug.ErrorReporter} The error reporter.
 */
goog.debug.ErrorReporter.install = function(
    loggingUrl, opt_contextProvider, opt_noAutoProtect) {
  var instance = new goog.debug.ErrorReporter(
      loggingUrl, opt_contextProvider, opt_noAutoProtect);
  return instance;
};


/**
 * Default implementation of XHR sender interface.
 *
 * @param {string} uri URI to make request to.
 * @param {string} method Send method.
 * @param {string} content Post data.
 * @param {Object|goog.structs.Map=} opt_headers Map of headers to add to the
 *     request.
 */
goog.debug.ErrorReporter.defaultXhrSender = function(uri, method, content,
    opt_headers) {
  goog.net.XhrIo.send(uri, null, method, content, opt_headers);
};


/**
 * Installs exception protection for an entry point function in addition
 * to those that are protected by default.
 * Has no effect in IE because window.onerror is used for reporting
 * exceptions in that case.
 *
 * @this {goog.debug.ErrorReporter}
 * @param {Function} fn An entry point function to be protected.
 * @return {Function} A protected wrapper function that calls the entry point
 *     function or null if the entry point could not be protected.
 */
goog.debug.ErrorReporter.prototype.protectAdditionalEntryPoint =
    goog.debug.ErrorReporter.ALLOW_AUTO_PROTECT ?
    function(fn) {
      if (this.errorHandler_) {
        return this.errorHandler_.protectEntryPoint(fn);
      }
      return null;
    } :
    function(fn) {
      goog.asserts.fail(
          'Cannot call protectAdditionalEntryPoint while ALLOW_AUTO_PROTECT ' +
          'is false.  If ALLOW_AUTO_PROTECT is false, the necessary ' +
          'auto-protect code in compiled/optimized mode is removed.');
      return null;
    };


if (goog.debug.ErrorReporter.ALLOW_AUTO_PROTECT) {
  /**
   * Sets up the error reporter.
   *
   * @private
   */
  goog.debug.ErrorReporter.prototype.setup_ = function() {
    if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('10')) {
      // Use "onerror" because caught exceptions in IE don't provide line
      // number.
      goog.debug.catchErrors(
          goog.bind(this.handleException, this), false, null);
    } else {
      // "onerror" doesn't work with FF2 or Chrome
      this.errorHandler_ = new goog.debug.ErrorHandler(
          goog.bind(this.handleException, this));

      this.errorHandler_.protectWindowSetTimeout();
      this.errorHandler_.protectWindowSetInterval();
      this.errorHandler_.protectWindowRequestAnimationFrame();
      goog.debug.entryPointRegistry.monitorAll(this.errorHandler_);
    }
  };
}


/**
 * Add headers to the logging url.
 * @param {Object|goog.structs.Map} loggingHeaders Extra headers to send
 *     to the logging URL.
 */
goog.debug.ErrorReporter.prototype.setLoggingHeaders =
    function(loggingHeaders) {
  this.extraHeaders_ = loggingHeaders;
};


/**
 * Set the function used to send error reports to the server.
 * @param {function(string, string, string, (Object|goog.structs.Map)=)}
 *     xhrSender If provided, this will be used to send a report to the
 *     server instead of the default method. The function will be given the URI,
 *     HTTP method request content, and (optionally) request headers to be
 *     added.
 */
goog.debug.ErrorReporter.prototype.setXhrSender = function(xhrSender) {
  this.xhrSender_ = xhrSender;
};


/**
 * Handler for caught exceptions. Sends report to the LoggingServlet and
 * notifies any listeners.
 *
 * @param {Object} e The exception.
 * @param {!Object<string, string>=} opt_context Context values to optionally
 *     include in the error report.
 */
goog.debug.ErrorReporter.prototype.handleException = function(e,
    opt_context) {
  var error = /** @type {!Error} */ (goog.debug.normalizeErrorObject(e));

  // Construct the context, possibly from the one provided in the argument, and
  // pass it to the context provider if there is one.
  var context = opt_context ? goog.object.clone(opt_context) : {};
  if (this.contextProvider_) {
    try {
      this.contextProvider_(error, context);
    } catch (err) {
      goog.log.error(goog.debug.ErrorReporter.logger_,
          'Context provider threw an exception: ' + err.message);
    }
  }
  // Truncate message to a reasonable length, since it will be sent in the URL.
  // The entire URL length historically needed to be 2,083 or less, so leave
  // some room for the rest of the URL.
  var message = error.message.substring(0, 1900);
  this.sendErrorReport(message, error.fileName, error.lineNumber, error.stack,
      context);

  try {
    this.dispatchEvent(
        new goog.debug.ErrorReporter.ExceptionEvent(error, context));
  } catch (ex) {
    // Swallow exception to avoid infinite recursion.
  }
};


/**
 * Sends an error report to the logging URL.  This will not consult the context
 * provider, the report will be sent exactly as specified.
 *
 * @param {string} message Error description.
 * @param {string} fileName URL of the JavaScript file with the error.
 * @param {number} line Line number of the error.
 * @param {string=} opt_trace Call stack trace of the error.
 * @param {!Object<string, string>=} opt_context Context information to include
 *     in the request.
 */
goog.debug.ErrorReporter.prototype.sendErrorReport =
    function(message, fileName, line, opt_trace, opt_context) {
  try {
    // Create the logging URL.
    var requestUrl = goog.uri.utils.appendParams(this.handlerUrl_,
        'script', fileName, 'error', message, 'line', line);

    if (!goog.object.isEmpty(this.additionalArguments_)) {
      requestUrl = goog.uri.utils.appendParamsFromMap(requestUrl,
          this.additionalArguments_);
    }

    var queryMap = {};
    queryMap['trace'] = opt_trace;

    // Copy context into query data map
    if (opt_context) {
      for (var entry in opt_context) {
        queryMap[this.contextPrefix_ + entry] = opt_context[entry];
      }
    }

    // Copy query data map into request.
    var queryData = goog.uri.utils.buildQueryDataFromMap(queryMap);

    // Truncate if truncationLimit set.
    if (goog.isNumber(this.truncationLimit_)) {
      queryData = queryData.substring(0, this.truncationLimit_);
    }

    // Send the request with the contents of the error.
    this.xhrSender_(requestUrl, 'POST', queryData, this.extraHeaders_);
  } catch (e) {
    var logMessage = goog.string.buildString(
        'Error occurred in sending an error report.\n\n',
        'script:', fileName, '\n',
        'line:', line, '\n',
        'error:', message, '\n',
        'trace:', opt_trace);
    goog.log.info(goog.debug.ErrorReporter.logger_, logMessage);
  }
};


/**
 * @param {string} prefix The prefix to appear prepended to all context
 *     variables in the error report body.
 */
goog.debug.ErrorReporter.prototype.setContextPrefix = function(prefix) {
  this.contextPrefix_ = prefix;
};


/**
 * @param {?number} limit Size in bytes to begin truncating POST body.  Set to
 *     null to prevent truncation.  The limit must be >= 0.
 */
goog.debug.ErrorReporter.prototype.setTruncationLimit = function(limit) {
  goog.asserts.assert(!goog.isNumber(limit) || limit >= 0,
      'Body limit must be valid number >= 0 or null');
  this.truncationLimit_ = limit;
};


/**
 * @param {!Object<string,string>} urlArgs Set of key-value pairs to append
 *     to handlerUrl_ before sending XHR.
 */
goog.debug.ErrorReporter.prototype.setAdditionalArguments = function(urlArgs) {
  this.additionalArguments_ = urlArgs;
};


/** @override */
goog.debug.ErrorReporter.prototype.disposeInternal = function() {
  if (goog.debug.ErrorReporter.ALLOW_AUTO_PROTECT) {
    goog.dispose(this.errorHandler_);
  }
  goog.debug.ErrorReporter.base(this, 'disposeInternal');
};
