Ersun Warncke пре 7 година
родитељ
комит
5a6e00e12b

+ 1 - 2
libraries/logger/index.js

@@ -1,2 +1 @@
-require("coffee-script/register")
-module.exports = require('./logging-manager');
+module.exports = require('./logging-manager.js');

+ 0 - 0
libraries/logger/test/unit/coffee/loggingManagerTests.coffee → libraries/logger/logger-tests-coffee/loggingManagerTests.coffee


+ 0 - 129
libraries/logger/logging-manager.coffee

@@ -1,129 +0,0 @@
-bunyan = require('bunyan')
-request = require('request')
-
-module.exports = Logger =
-	initialize: (name) ->
-		isProduction = process.env['NODE_ENV']?.toLowerCase() == 'production'
-		@defaultLevel = process.env['LOG_LEVEL'] or if isProduction then "warn" else  "debug"
-		@loggerName = name
-		@logger = bunyan.createLogger
-			name: name
-			serializers: bunyan.stdSerializers
-			level: @defaultLevel
-		if isProduction
-			# check for log level override on startup
-			@.checkLogLevel()
-			# re-check log level every minute
-			checkLogLevel = () => @.checkLogLevel()
-			setInterval(checkLogLevel, 1000 * 60)
-		return @
-
-	checkLogLevel: () ->
-		options =
-			headers:
-				"Metadata-Flavor": "Google"
-			uri: "http://metadata.google.internal/computeMetadata/v1/project/attributes/#{@loggerName}-setLogLevelEndTime"
-		request options, (err, response, body) =>
-			if parseInt(body) > Date.now()
-				@logger.level('trace')
-			else
-				@logger.level(@defaultLevel)
-
-	initializeErrorReporting: (sentry_dsn, options) ->
-		raven = require "raven"
-		@raven = new raven.Client(sentry_dsn, options)
-		@lastErrorTimeStamp = 0 # for rate limiting on sentry reporting
-		@lastErrorCount = 0
-
-	captureException: (attributes, message, level) ->
-		# handle case of logger.error "message"
-		if typeof attributes is 'string'
-			attributes = {err: new Error(attributes)}
-		# extract any error object
-		error = attributes.err or attributes.error
-		# avoid reporting errors twice
-		for key, value of attributes
-			return if value instanceof Error && value.reportedToSentry
-		# include our log message in the error report
-		if not error?
-			error = {message: message} if typeof message is 'string'
-		else if message?
-			attributes.description = message
-		# report the error
-		if error?
-			# capture attributes and use *_id objects as tags
-			tags = {}
-			extra = {}
-			for key, value of attributes
-				tags[key] = value if key.match(/_id/) and typeof value == 'string'
-				extra[key] = value
-			# capture req object if available
-			req = attributes.req
-			if req?
-				extra.req =
-					method: req.method
-					url: req.originalUrl
-					query: req.query
-					headers: req.headers
-					ip: req.ip
-			# recreate error objects that have been converted to a normal object
-			if !(error instanceof Error) and typeof error is "object"
-				newError = new Error(error.message)
-				for own key, value of error
-					newError[key] = value
-				error = newError
-			# filter paths from the message to avoid duplicate errors in sentry
-			# (e.g. errors from `fs` methods which have a path attribute)
-			try
-				error.message = error.message.replace(" '#{error.path}'", '') if error.path
-			# send the error to sentry
-			try
-				@raven.captureException(error, {tags: tags, extra: extra, level: level})
-				# put a flag on the errors to avoid reporting them multiple times
-				for key, value of attributes
-					value.reportedToSentry = true if value instanceof Error
-			catch
-				return # ignore any errors
-
-	debug : () ->
-		@logger.debug.apply(@logger, arguments)
-	info : ()->
-		@logger.info.apply(@logger, arguments)
-	log : ()->
-		@logger.info.apply(@logger, arguments)
-	error: (attributes, message, args...)->
-		@logger.error(attributes, message, args...)
-		if @raven?
-			MAX_ERRORS = 5 # maximum number of errors in 1 minute
-			now = new Date()
-			# have we recently reported an error?
-			recentSentryReport = (now - @lastErrorTimeStamp) < 60 * 1000
-			# if so, increment the error count
-			if recentSentryReport
-				@lastErrorCount++
-			else
-				@lastErrorCount = 0
-				@lastErrorTimeStamp = now
-			# only report 5 errors every minute to avoid overload
-			if @lastErrorCount < MAX_ERRORS
-				# add a note if the rate limit has been hit
-				note = if @lastErrorCount+1 is MAX_ERRORS then "(rate limited)" else ""
-				# report the exception
-				@captureException(attributes, message, "error#{note}")
-	err: () ->
-		@error.apply(this, arguments)
-	warn: ()->
-		@logger.warn.apply(@logger, arguments)
-	fatal: (attributes, message, callback = () ->) ->
-		@logger.fatal(attributes, message)
-		if @raven?
-			cb = (e) -> # call the callback once after 'logged' or 'error' event
-				callback()
-				cb = () ->
-			@captureException(attributes, message, "fatal")
-			@raven.once 'logged', cb
-			@raven.once 'error', cb
-		else
-			callback()
-
-Logger.initialize("default-sharelatex")

+ 221 - 0
libraries/logger/logging-manager.js

@@ -0,0 +1,221 @@
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS203: Remove `|| {}` from converted for-own loops
+ * DS205: Consider reworking code to avoid use of IIFEs
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+
+const bunyan = require('bunyan');
+const request = require('request');
+
+const LOG_METHODS = [
+	'debug', 'info', 'log', 'error', 'err', 'warn', 'fatal'
+]
+
+class Logger {
+
+	constructor(name) {
+		const isProduction = (process.env['NODE_ENV'] || '').toLowerCase() === 'production';
+		this.defaultLevel = process.env['LOG_LEVEL'] || (isProduction ? "warn" :  "debug");
+		this.loggerName = name;
+		this.logger = bunyan.createLogger({
+			name,
+			serializers: bunyan.stdSerializers,
+			level: this.defaultLevel
+		});
+		if (isProduction) {
+			// check for log level override on startup
+			this.checkLogLevel();
+			// re-check log level every minute
+			const checkLogLevel = () => this.checkLogLevel();
+			setInterval(checkLogLevel, 1000 * 60);
+		}
+		return this;		
+	}
+
+	checkLogLevel() {
+		const options = {
+			headers: {
+				"Metadata-Flavor": "Google"
+			},
+			uri: `http://metadata.google.internal/computeMetadata/v1/project/attributes/${this.loggerName}-setLogLevelEndTime`
+		};
+		return request(options, (err, response, body) => {
+			if (parseInt(body) > Date.now()) {
+				return this.logger.level('trace');
+			} else {
+				return this.logger.level(this.defaultLevel);
+			}
+		});
+	}
+
+	initializeErrorReporting(sentry_dsn, options) {
+		const raven = require("raven");
+		this.raven = new raven.Client(sentry_dsn, options);
+		this.lastErrorTimeStamp = 0; // for rate limiting on sentry reporting
+		return this.lastErrorCount = 0;
+	}
+
+	captureException(attributes, message, level) {
+		// handle case of logger.error "message"
+		let key, value;
+		if (typeof attributes === 'string') {
+			attributes = {err: new Error(attributes)};
+		}
+		// extract any error object
+		let error = attributes.err || attributes.error;
+		// avoid reporting errors twice
+		for (key in attributes) {
+			value = attributes[key];
+			if (value instanceof Error && value.reportedToSentry) { return; }
+		}
+		// include our log message in the error report
+		if ((error == null)) {
+			if (typeof message === 'string') { error = {message}; }
+		} else if (message != null) {
+			attributes.description = message;
+		}
+		// report the error
+		if (error != null) {
+			// capture attributes and use *_id objects as tags
+			const tags = {};
+			const extra = {};
+			for (key in attributes) {
+				value = attributes[key];
+				if (key.match(/_id/) && (typeof value === 'string')) { tags[key] = value; }
+				extra[key] = value;
+			}
+			// capture req object if available
+			const { req } = attributes;
+			if (req != null) {
+				extra.req = {
+					method: req.method,
+					url: req.originalUrl,
+					query: req.query,
+					headers: req.headers,
+					ip: req.ip
+				};
+			}
+			// recreate error objects that have been converted to a normal object
+			if (!(error instanceof Error) && (typeof error === "object")) {
+				const newError = new Error(error.message);
+				for (key of Object.keys(error || {})) {
+					value = error[key];
+					newError[key] = value;
+				}
+				error = newError;
+			}
+			// filter paths from the message to avoid duplicate errors in sentry
+			// (e.g. errors from `fs` methods which have a path attribute)
+			try {
+				if (error.path) { error.message = error.message.replace(` '${error.path}'`, ''); }
+			} catch (error1) {}
+			// send the error to sentry
+			try {
+				this.raven.captureException(error, {tags, extra, level});
+				// put a flag on the errors to avoid reporting them multiple times
+				return (() => {
+					const result = [];
+					for (key in attributes) {
+						value = attributes[key];
+						if (value instanceof Error) { result.push(value.reportedToSentry = true); } else {
+							result.push(undefined);
+						}
+					}
+					return result;
+				})();
+			} catch (error2) {
+				return;
+			}
+		}
+	}
+
+	debug() {
+		return this.logger.debug.apply(this.logger, arguments);
+	}
+
+	info(){
+		return this.logger.info.apply(this.logger, arguments);
+	}
+
+	log(){
+		return this.logger.info.apply(this.logger, arguments);
+	}
+
+	error(attributes, message, ...args){
+		this.logger.error(attributes, message, ...Array.from(args));
+		if (this.raven != null) {
+			const MAX_ERRORS = 5; // maximum number of errors in 1 minute
+			const now = new Date();
+			// have we recently reported an error?
+			const recentSentryReport = (now - this.lastErrorTimeStamp) < (60 * 1000);
+			// if so, increment the error count
+			if (recentSentryReport) {
+				this.lastErrorCount++;
+			} else {
+				this.lastErrorCount = 0;
+				this.lastErrorTimeStamp = now;
+			}
+			// only report 5 errors every minute to avoid overload
+			if (this.lastErrorCount < MAX_ERRORS) {
+				// add a note if the rate limit has been hit
+				const note = (this.lastErrorCount+1) === MAX_ERRORS ? "(rate limited)" : "";
+				// report the exception
+				return this.captureException(attributes, message, `error${note}`);
+			}
+		}
+	}
+
+	err() {
+		return this.error.apply(this, arguments);
+	}
+
+	warn(){
+		return this.logger.warn.apply(this.logger, arguments);
+	}
+
+	fatal(attributes, message, callback) {
+		if (callback == null) { callback = function() {}; }
+		this.logger.fatal(attributes, message);
+		if (this.raven != null) {
+			var cb = function(e) { // call the callback once after 'logged' or 'error' event
+				callback();
+				return cb = function() {};
+			};
+			this.captureException(attributes, message, "fatal");
+			this.raven.once('logged', cb);
+			return this.raven.once('error', cb);
+		} else {
+			return callback();
+		}
+	}
+}
+
+let defaultLogger
+// initialize default logger if not already initialized
+if (!global.__logger_sharelatex__default_logger__) {
+	global.__logger_sharelatex__default_logger__ = defaultLogger = new Logger("default-sharelatex")
+}
+else {
+	defaultLogger = global.__logger_sharelatex__default_logger__
+}
+
+// support old interface for creating new Logger instances
+Logger.initialize = function initialize(name) {
+	return new Logger(name)
+}
+// add a static method for each log method that will use the default logger
+for (const logMethod of LOG_METHODS) {
+	Logger[logMethod] = function () {
+		return defaultLogger[logMethod].apply(defaultLogger, arguments)
+	}
+}
+// return default logger
+Logger.defaultLogger = function defaultLogger () {
+	return defaultLogger
+}
+
+module.exports = Logger

+ 826 - 0
libraries/logger/package-lock.json

@@ -0,0 +1,826 @@
+{
+  "name": "logger-sharelatex",
+  "version": "1.6.0",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "@sinonjs/commons": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz",
+      "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==",
+      "dev": true,
+      "requires": {
+        "type-detect": "4.0.8"
+      }
+    },
+    "@sinonjs/formatio": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz",
+      "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==",
+      "dev": true,
+      "requires": {
+        "@sinonjs/commons": "^1",
+        "@sinonjs/samsam": "^3.1.0"
+      }
+    },
+    "@sinonjs/samsam": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.2.0.tgz",
+      "integrity": "sha512-j5F1rScewLtx6pbTK0UAjA3jJj4RYiSKOix53YWv+Jzy/AZ69qHxUpU8fwVLjyKbEEud9QrLpv6Ggs7WqTimYw==",
+      "dev": true,
+      "requires": {
+        "@sinonjs/commons": "^1.0.2",
+        "array-from": "^2.1.1",
+        "lodash": "^4.17.11"
+      }
+    },
+    "@sinonjs/text-encoding": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz",
+      "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
+      "dev": true
+    },
+    "ajv": {
+      "version": "6.10.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
+      "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
+      "requires": {
+        "fast-deep-equal": "^2.0.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      }
+    },
+    "array-from": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz",
+      "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=",
+      "dev": true
+    },
+    "asn1": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+      "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
+      "requires": {
+        "safer-buffer": "~2.1.0"
+      }
+    },
+    "assert-plus": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+      "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
+    },
+    "assertion-error": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+      "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+      "dev": true
+    },
+    "asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
+    },
+    "aws-sign2": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+      "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
+    },
+    "aws4": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
+      "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
+    },
+    "balanced-match": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
+    },
+    "bcrypt-pbkdf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+      "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+      "requires": {
+        "tweetnacl": "^0.14.3"
+      }
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "requires": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "browser-stdout": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+      "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+      "dev": true
+    },
+    "bunyan": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz",
+      "integrity": "sha1-X259RMQ7lS9WsPQTCeOrEjkbTi0=",
+      "requires": {
+        "dtrace-provider": "~0.6",
+        "mv": "~2",
+        "safe-json-stringify": "~1"
+      }
+    },
+    "caseless": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+      "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
+    },
+    "chai": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz",
+      "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==",
+      "dev": true,
+      "requires": {
+        "assertion-error": "^1.1.0",
+        "check-error": "^1.0.2",
+        "deep-eql": "^3.0.1",
+        "get-func-name": "^2.0.0",
+        "pathval": "^1.1.0",
+        "type-detect": "^4.0.5"
+      }
+    },
+    "check-error": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
+      "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
+      "dev": true
+    },
+    "combined-stream": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
+      "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
+      "requires": {
+        "delayed-stream": "~1.0.0"
+      }
+    },
+    "commander": {
+      "version": "2.15.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
+      "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
+      "dev": true
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+    },
+    "cookie": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+      "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
+    },
+    "core-util-is": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+    },
+    "dashdash": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+      "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "debug": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+      "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+      "dev": true,
+      "requires": {
+        "ms": "2.0.0"
+      }
+    },
+    "deep-eql": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
+      "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
+      "dev": true,
+      "requires": {
+        "type-detect": "^4.0.0"
+      }
+    },
+    "delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
+    },
+    "diff": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+      "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+      "dev": true
+    },
+    "dtrace-provider": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz",
+      "integrity": "sha1-CweNVReTfYcxAUUtkUZzdVe3XlE=",
+      "optional": true,
+      "requires": {
+        "nan": "^2.0.8"
+      }
+    },
+    "ecc-jsbn": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+      "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
+      "requires": {
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.1.0"
+      }
+    },
+    "escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+      "dev": true
+    },
+    "extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
+    },
+    "extsprintf": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+      "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
+    },
+    "fast-deep-equal": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+      "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
+    },
+    "fast-json-stable-stringify": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+      "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
+    },
+    "forever-agent": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+      "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
+    },
+    "form-data": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
+      "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
+      "requires": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.6",
+        "mime-types": "^2.1.12"
+      }
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+      "dev": true
+    },
+    "get-func-name": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
+      "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
+      "dev": true
+    },
+    "getpass": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+      "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "glob": {
+      "version": "6.0.4",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
+      "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
+      "optional": true,
+      "requires": {
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "2 || 3",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      }
+    },
+    "growl": {
+      "version": "1.10.5",
+      "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
+      "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
+      "dev": true
+    },
+    "har-schema": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
+      "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
+    },
+    "har-validator": {
+      "version": "5.1.3",
+      "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
+      "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
+      "requires": {
+        "ajv": "^6.5.5",
+        "har-schema": "^2.0.0"
+      }
+    },
+    "has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+      "dev": true
+    },
+    "he": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
+      "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
+      "dev": true
+    },
+    "http-signature": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
+      "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "jsprim": "^1.2.2",
+        "sshpk": "^1.7.0"
+      }
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "requires": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "inherits": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+    },
+    "is-typedarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+      "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
+    },
+    "isarray": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+      "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+      "dev": true
+    },
+    "isstream": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+      "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
+    },
+    "jsbn": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+      "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
+    },
+    "json-schema": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
+      "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
+    },
+    "json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
+    },
+    "json-stringify-safe": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
+    },
+    "jsprim": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
+      "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+      "requires": {
+        "assert-plus": "1.0.0",
+        "extsprintf": "1.3.0",
+        "json-schema": "0.2.3",
+        "verror": "1.10.0"
+      }
+    },
+    "just-extend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz",
+      "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==",
+      "dev": true
+    },
+    "lodash": {
+      "version": "4.17.11",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+      "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
+      "dev": true
+    },
+    "lolex": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/lolex/-/lolex-3.1.0.tgz",
+      "integrity": "sha512-zFo5MgCJ0rZ7gQg69S4pqBsLURbFw11X68C18OcJjJQbqaXm2NoTrGl1IMM3TIz0/BnN1tIs2tzmmqvCsOMMjw==",
+      "dev": true
+    },
+    "lsmod": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/lsmod/-/lsmod-1.0.0.tgz",
+      "integrity": "sha1-mgD3bco26yP6BTUK/htYXUKZ5ks="
+    },
+    "mime-db": {
+      "version": "1.38.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz",
+      "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg=="
+    },
+    "mime-types": {
+      "version": "2.1.22",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz",
+      "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==",
+      "requires": {
+        "mime-db": "~1.38.0"
+      }
+    },
+    "minimatch": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+      "requires": {
+        "brace-expansion": "^1.1.7"
+      }
+    },
+    "minimist": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+      "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
+    },
+    "mkdirp": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+      "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+      "requires": {
+        "minimist": "0.0.8"
+      }
+    },
+    "mocha": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz",
+      "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==",
+      "dev": true,
+      "requires": {
+        "browser-stdout": "1.3.1",
+        "commander": "2.15.1",
+        "debug": "3.1.0",
+        "diff": "3.5.0",
+        "escape-string-regexp": "1.0.5",
+        "glob": "7.1.2",
+        "growl": "1.10.5",
+        "he": "1.1.1",
+        "minimatch": "3.0.4",
+        "mkdirp": "0.5.1",
+        "supports-color": "5.4.0"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+          "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.4",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        }
+      }
+    },
+    "ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+      "dev": true
+    },
+    "mv": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
+      "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=",
+      "optional": true,
+      "requires": {
+        "mkdirp": "~0.5.1",
+        "ncp": "~2.0.0",
+        "rimraf": "~2.4.0"
+      }
+    },
+    "nan": {
+      "version": "2.12.1",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz",
+      "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==",
+      "optional": true
+    },
+    "ncp": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
+      "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=",
+      "optional": true
+    },
+    "nise": {
+      "version": "1.4.10",
+      "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.10.tgz",
+      "integrity": "sha512-sa0RRbj53dovjc7wombHmVli9ZihXbXCQ2uH3TNm03DyvOSIQbxg+pbqDKrk2oxMK1rtLGVlKxcB9rrc6X5YjA==",
+      "dev": true,
+      "requires": {
+        "@sinonjs/formatio": "^3.1.0",
+        "@sinonjs/text-encoding": "^0.7.1",
+        "just-extend": "^4.0.2",
+        "lolex": "^2.3.2",
+        "path-to-regexp": "^1.7.0"
+      },
+      "dependencies": {
+        "lolex": {
+          "version": "2.7.5",
+          "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz",
+          "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==",
+          "dev": true
+        }
+      }
+    },
+    "oauth-sign": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
+      "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "requires": {
+        "wrappy": "1"
+      }
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
+    },
+    "path-to-regexp": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
+      "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
+      "dev": true,
+      "requires": {
+        "isarray": "0.0.1"
+      }
+    },
+    "pathval": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
+      "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
+      "dev": true
+    },
+    "performance-now": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+      "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
+    },
+    "psl": {
+      "version": "1.1.31",
+      "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz",
+      "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw=="
+    },
+    "punycode": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
+    },
+    "qs": {
+      "version": "6.5.2",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+      "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
+    },
+    "raven": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/raven/-/raven-1.2.1.tgz",
+      "integrity": "sha1-lJwTTbAooZC3u/j3kKrlQbfAIL0=",
+      "requires": {
+        "cookie": "0.3.1",
+        "json-stringify-safe": "5.0.1",
+        "lsmod": "1.0.0",
+        "stack-trace": "0.0.9",
+        "uuid": "3.0.0"
+      }
+    },
+    "request": {
+      "version": "2.88.0",
+      "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
+      "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
+      "requires": {
+        "aws-sign2": "~0.7.0",
+        "aws4": "^1.8.0",
+        "caseless": "~0.12.0",
+        "combined-stream": "~1.0.6",
+        "extend": "~3.0.2",
+        "forever-agent": "~0.6.1",
+        "form-data": "~2.3.2",
+        "har-validator": "~5.1.0",
+        "http-signature": "~1.2.0",
+        "is-typedarray": "~1.0.0",
+        "isstream": "~0.1.2",
+        "json-stringify-safe": "~5.0.1",
+        "mime-types": "~2.1.19",
+        "oauth-sign": "~0.9.0",
+        "performance-now": "^2.1.0",
+        "qs": "~6.5.2",
+        "safe-buffer": "^5.1.2",
+        "tough-cookie": "~2.4.3",
+        "tunnel-agent": "^0.6.0",
+        "uuid": "^3.3.2"
+      },
+      "dependencies": {
+        "uuid": {
+          "version": "3.3.2",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+          "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
+        }
+      }
+    },
+    "require-like": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz",
+      "integrity": "sha1-rW8wwTvs15cBDEaK+ndcDAprR/o=",
+      "dev": true
+    },
+    "rimraf": {
+      "version": "2.4.5",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
+      "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=",
+      "optional": true,
+      "requires": {
+        "glob": "^6.0.1"
+      }
+    },
+    "safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+    },
+    "safe-json-stringify": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz",
+      "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==",
+      "optional": true
+    },
+    "safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+    },
+    "sandboxed-module": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-2.0.3.tgz",
+      "integrity": "sha1-x+VFkzm7y6KMUwPusz9ug4e/upY=",
+      "dev": true,
+      "requires": {
+        "require-like": "0.1.2",
+        "stack-trace": "0.0.9"
+      }
+    },
+    "sinon": {
+      "version": "7.2.7",
+      "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.2.7.tgz",
+      "integrity": "sha512-rlrre9F80pIQr3M36gOdoCEWzFAMDgHYD8+tocqOw+Zw9OZ8F84a80Ds69eZfcjnzDqqG88ulFld0oin/6rG/g==",
+      "dev": true,
+      "requires": {
+        "@sinonjs/commons": "^1.3.1",
+        "@sinonjs/formatio": "^3.2.1",
+        "@sinonjs/samsam": "^3.2.0",
+        "diff": "^3.5.0",
+        "lolex": "^3.1.0",
+        "nise": "^1.4.10",
+        "supports-color": "^5.5.0"
+      },
+      "dependencies": {
+        "supports-color": {
+          "version": "5.5.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+          "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "sinon-chai": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.3.0.tgz",
+      "integrity": "sha512-r2JhDY7gbbmh5z3Q62pNbrjxZdOAjpsqW/8yxAZRSqLZqowmfGZPGUZPFf3UX36NLis0cv8VEM5IJh9HgkSOAA==",
+      "dev": true
+    },
+    "sshpk": {
+      "version": "1.16.1",
+      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
+      "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
+      "requires": {
+        "asn1": "~0.2.3",
+        "assert-plus": "^1.0.0",
+        "bcrypt-pbkdf": "^1.0.0",
+        "dashdash": "^1.12.0",
+        "ecc-jsbn": "~0.1.1",
+        "getpass": "^0.1.1",
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.0.2",
+        "tweetnacl": "~0.14.0"
+      }
+    },
+    "stack-trace": {
+      "version": "0.0.9",
+      "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz",
+      "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU="
+    },
+    "supports-color": {
+      "version": "5.4.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
+      "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
+      "dev": true,
+      "requires": {
+        "has-flag": "^3.0.0"
+      }
+    },
+    "tough-cookie": {
+      "version": "2.4.3",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
+      "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
+      "requires": {
+        "psl": "^1.1.24",
+        "punycode": "^1.4.1"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+          "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
+        }
+      }
+    },
+    "tunnel-agent": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+      "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+      "requires": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "tweetnacl": {
+      "version": "0.14.5",
+      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+      "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
+    },
+    "type-detect": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+      "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+      "dev": true
+    },
+    "uri-js": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
+      "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
+      "requires": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "uuid": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz",
+      "integrity": "sha1-Zyj8BFnEUNeWqZwxg3VpvfZy1yg="
+    },
+    "verror": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+      "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "core-util-is": "1.0.2",
+        "extsprintf": "^1.2.0"
+      }
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+    }
+  }
+}

+ 2 - 3
libraries/logger/package.json

@@ -8,18 +8,17 @@
   },
   "version": "1.6.0",
   "scripts": {
-    "test": "mocha --require coffee-script/register test/**/*.coffee"
+    "test": "mocha test/**/*.js"
   },
   "dependencies": {
     "bunyan": "1.5.1",
-    "coffee-script": "1.12.4",
     "raven": "^1.1.3",
     "request": "^2.88.0"
   },
   "devDependencies": {
     "chai": "^4.2.0",
     "mocha": "^5.2.0",
-    "sandboxed-module": "0.2.0",
+    "sandboxed-module": "2.0.3",
     "sinon": "^7.2.3",
     "sinon-chai": "^3.3.0"
   }

+ 302 - 0
libraries/logger/test/unit/loggingManagerTests.js

@@ -0,0 +1,302 @@
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const SandboxedModule = require("sandboxed-module");
+const chai = require("chai");
+const path = require("path");
+const sinon = require("sinon");
+const sinonChai = require("sinon-chai");
+
+chai.use(sinonChai);
+chai.should();
+
+const modulePath = path.join(__dirname, '../../logging-manager.js');
+
+describe('LoggingManager', function() {
+
+	beforeEach(function() {
+		this.start = Date.now();
+		this.clock = sinon.useFakeTimers(this.start);
+		this.captureException = sinon.stub();
+		this.mockBunyanLogger = {
+			debug: sinon.stub(),
+			error: sinon.stub(),
+			fatal: sinon.stub(),
+			info: sinon.stub(),
+			level: sinon.stub(),
+			warn: sinon.stub()
+		};
+		this.mockRavenClient = {
+			captureException: this.captureException,
+			once: sinon.stub().yields()
+		};
+		this.LoggingManager = SandboxedModule.require(modulePath, {
+			globals: { console },
+			requires: {
+				bunyan: (this.Bunyan = {createLogger: sinon.stub().returns(this.mockBunyanLogger)}),
+				raven: (this.Raven = {Client: sinon.stub().returns(this.mockRavenClient)}),
+				request: (this.Request = sinon.stub()),
+			},
+		});
+		this.loggerName = "test";
+		this.logger = this.LoggingManager.initialize(this.loggerName);
+		this.logger.initializeErrorReporting("test_dsn");
+	});
+
+	afterEach(function() {
+		this.clock.restore();
+	});
+
+	describe('initialize', function() {
+		beforeEach(function() {
+			this.checkLogLevelStub = sinon.stub(this.LoggingManager.prototype, "checkLogLevel");
+			this.Bunyan.createLogger.reset();
+		});
+
+		afterEach(function () {
+			this.checkLogLevelStub.restore()
+		})
+
+		describe("not in production", function() {
+			beforeEach(function() {
+				this.logger = this.LoggingManager.initialize(this.loggerName)
+			});
+
+			it('should default to log level debug', function() {
+				this.Bunyan.createLogger.should.have.been.calledWithMatch({level: "debug"});
+			});
+
+			it('should not run checkLogLevel', function() {
+				this.checkLogLevelStub.should.not.have.been.called;
+			});
+		});
+
+		describe("in production", function() {
+			beforeEach(function() {
+				process.env.NODE_ENV = 'production';
+				this.logger = this.LoggingManager.initialize(this.loggerName)
+			});
+
+			afterEach(() => delete process.env.NODE_ENV);
+
+			it('should default to log level warn', function() {
+				this.Bunyan.createLogger.should.have.been.calledWithMatch({level: "warn"});
+			});
+
+			it('should run checkLogLevel', function() {
+				this.checkLogLevelStub.should.have.been.calledOnce;
+			});
+
+			describe('after 1 minute', () =>
+				it('should run checkLogLevel again', function() {
+					this.clock.tick(61*1000);
+					this.checkLogLevelStub.should.have.been.calledTwice;
+				})
+			);
+
+			describe('after 2 minutes', () =>
+				it('should run checkLogLevel again', function() {
+					this.clock.tick(121*1000);
+					this.checkLogLevelStub.should.have.been.calledThrice;
+				})
+			);
+		});
+
+		describe("when LOG_LEVEL set in env", function() {
+			beforeEach(function() {
+				process.env.LOG_LEVEL = "trace";
+				this.LoggingManager.initialize();
+			});
+
+			afterEach(() => delete process.env.LOG_LEVEL);
+
+			it("should use custom log level", function() {
+				this.Bunyan.createLogger.should.have.been.calledWithMatch({level: "trace"});
+			});
+		});
+	});
+
+	describe('bunyan logging', function() {
+		beforeEach(function() {
+			this.logArgs = [ {foo: "bar"}, "foo", "bar" ];});
+
+		it('should log debug', function() {
+			this.logger.debug(this.logArgs);
+			this.mockBunyanLogger.debug.should.have.been.calledWith(this.logArgs);
+		});
+
+		it('should log error', function() {
+			this.logger.error(this.logArgs);
+			this.mockBunyanLogger.error.should.have.been.calledWith(this.logArgs);
+		});
+
+		it('should log fatal', function() {
+			this.logger.fatal(this.logArgs);
+			this.mockBunyanLogger.fatal.should.have.been.calledWith(this.logArgs);
+		});
+
+		it('should log info', function() {
+			this.logger.info(this.logArgs);
+			this.mockBunyanLogger.info.should.have.been.calledWith(this.logArgs);
+		});
+
+		it('should log warn', function() {
+			this.logger.warn(this.logArgs);
+			this.mockBunyanLogger.warn.should.have.been.calledWith(this.logArgs);
+		});
+
+		it('should log err', function() {
+			this.logger.err(this.logArgs);
+			this.mockBunyanLogger.error.should.have.been.calledWith(this.logArgs);
+		});
+
+		it('should log log', function() {
+			this.logger.log(this.logArgs);
+			this.mockBunyanLogger.info.should.have.been.calledWith(this.logArgs);
+		});
+	});
+
+	describe('logger.error', function() {
+		it('should report a single error to sentry', function() {
+			this.logger.error({foo:'bar'}, "message");
+			this.captureException.called.should.equal(true);
+		});
+
+		it('should report the same error to sentry only once', function() {
+			const error1 = new Error('this is the error');
+			this.logger.error({foo: error1}, "first message");
+			this.logger.error({bar: error1}, "second message");
+			this.captureException.callCount.should.equal(1);
+		});
+
+		it('should report two different errors to sentry individually', function() {
+			const error1 = new Error('this is the error');
+			const error2 = new Error('this is the error');
+			this.logger.error({foo: error1}, "first message");
+			this.logger.error({bar: error2}, "second message");
+			this.captureException.callCount.should.equal(2);
+		});
+
+		it('should remove the path from fs errors', function() {
+			const fsError = new Error("Error: ENOENT: no such file or directory, stat '/tmp/3279b8d0-da10-11e8-8255-efd98985942b'");
+			fsError.path = "/tmp/3279b8d0-da10-11e8-8255-efd98985942b";
+			this.logger.error({err: fsError}, "message");
+			this.captureException.calledWith(sinon.match.has('message', 'Error: ENOENT: no such file or directory, stat')).should.equal(true);
+		});
+
+		it('for multiple errors should only report a maximum of 5 errors to sentry', function() {
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			this.captureException.callCount.should.equal(5);
+		});
+
+		it('for multiple errors with a minute delay should report 10 errors to sentry', function() {
+			// the first five errors should be reported to sentry
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			// the following errors should not be reported
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			// allow a minute to pass
+			this.clock.tick(this.start+ (61*1000));
+			// after a minute the next five errors should be reported to sentry
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			// the following errors should not be reported to sentry
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			this.logger.error({foo:'bar'}, "message");
+			this.captureException.callCount.should.equal(10);
+		});
+	});
+
+	describe('checkLogLevel', function() {
+		it('should request log level override from google meta data service', function() {
+			this.logger.checkLogLevel();
+			const options = {
+				headers: {
+					"Metadata-Flavor": "Google"
+				},
+				uri: `http://metadata.google.internal/computeMetadata/v1/project/attributes/${this.loggerName}-setLogLevelEndTime`
+			};
+			this.Request.should.have.been.calledWithMatch(options);
+		});
+
+		describe('when request has error', function() {
+			beforeEach(function() {
+				this.Request.yields("error");
+				this.logger.checkLogLevel();
+			});
+
+			it("should only set default level", function() {
+				this.mockBunyanLogger.level.should.have.been.calledOnce.and.calledWith('debug');
+			});
+		});
+
+		describe('when statusCode is not 200', function() {
+			beforeEach(function() {
+				this.Request.yields(null, {statusCode: 404});
+				this.logger.checkLogLevel();
+			});
+
+			it("should only set default level", function() {
+				this.mockBunyanLogger.level.should.have.been.calledOnce.and.calledWith('debug');
+			});
+		});
+
+		describe('when time value returned that is less than current time', function() {
+			beforeEach(function() {
+				this.Request.yields(null, {statusCode: 200}, '1');
+				this.logger.checkLogLevel();
+			});
+
+			it("should only set default level", function() {
+				this.mockBunyanLogger.level.should.have.been.calledOnce.and.calledWith('debug');
+			});
+		});
+
+		describe('when time value returned that is less than current time', function() {
+			describe('when level is already set', function() {
+				beforeEach(function() {
+					this.mockBunyanLogger.level.returns(10);
+					this.Request.yields(null, {statusCode: 200}, this.start + 1000);
+					this.logger.checkLogLevel();
+				});
+
+				it("should set trace level", function() {
+					this.mockBunyanLogger.level.should.have.been.calledOnce.and.calledWith('trace');
+				});
+			});
+
+			describe('when level is not already set', function() {
+				beforeEach(function() {
+					this.mockBunyanLogger.level.returns(20);
+					this.Request.yields(null, {statusCode: 200}, this.start + 1000);
+					this.logger.checkLogLevel();
+				});
+
+				it("should set trace level", function() {
+					this.mockBunyanLogger.level.should.have.been.calledOnce.and.calledWith('trace');
+				});
+			});
+		});
+	});
+});