import * as assertions from '#core/assert/base'; import { createError, createExpectedError, duplicateErrorIfNecessary, } from '#core/error'; import { USER_ERROR_EMBED_SENTINEL, USER_ERROR_SENTINEL, elementStringOrPassThru, isUserErrorMessage, stripUserError, } from '#core/error/message-helpers'; import * as mode from '#core/mode'; import {isArray, isString} from '#core/types'; import {once} from '#core/types/function'; import {getHashParams} from '#core/types/string/url'; import * as urls from '../config/urls'; import {getMode} from '../mode'; const noop = () => {}; // These are exported here despite being defined elswhere to avoid updating // imports across many files for now. export {USER_ERROR_SENTINEL, isUserErrorMessage}; /** * Sets reportError function. Called from error-reporting.js to break cyclic * dependency. * @param {function(this:Window, Error, (?Element)=): ?|undefined} fn */ export function setReportError(fn) { self.__AMP_REPORT_ERROR = fn; } /** * @enum {number} */ export const LogLevel_Enum = { OFF: 0, ERROR: 1, WARN: 2, INFO: 3, FINE: 4, }; /** * @type {!LogLevel_Enum|undefined} * @private */ let levelOverride_ = undefined; /** * @param {!LogLevel_Enum} level */ export function overrideLogLevel(level) { levelOverride_ = level; } /** * Prefixes `internalRuntimeVersion` with the `01` channel signifier (for prod.) for * extracted message URLs. * (Specific channel is irrelevant: message tables are invariant on internal version.) * @return {string} */ const messageUrlRtv = () => `01${mode.version()}`; /** * Gets a URL to display a message on amp.dev. * @param {string} id * @param {!Array} interpolatedParts * @return {string} */ const externalMessageUrl = (id, interpolatedParts) => interpolatedParts.reduce( (prefix, arg) => `${prefix}&s[]=${messageArgToEncodedComponent(arg)}`, `https://log.amp.dev/?v=${messageUrlRtv()}&id=${encodeURIComponent(id)}` ); /** * URL to simple log messages table JSON file, which contains an {[key: string]: string} * which maps message id to full message template. * @return {string} */ const externalMessagesSimpleTableUrl = () => `${urls.cdn}/rtv/${messageUrlRtv()}/log-messages.simple.json`; /** * @param {*} arg * @return {string} */ const messageArgToEncodedComponent = (arg) => encodeURIComponent(String(elementStringOrPassThru(arg))); /** * @param {!Window=} opt_win * @return {number} */ export const logHashParam = (opt_win) => parseInt(getHashParams(opt_win)['log'], 10); /** * Logging class. Use of sentinel string instead of a boolean to check user/dev * errors because errors could be rethrown by some native code as a new error, * and only a message would survive. Also, some browser don’t support a 5th * error object argument in window.onerror. List of supporting browser can be * found here: * https://blog.sentry.io/2016/01/04/client-javascript-reporting-window-onerror.html * @final * @private Visible for testing only. */ export class Log { /** * opt_suffix will be appended to error message to identify the type of the * error message. We can't rely on the error object to pass along the type * because some browsers do not have this param in its window.onerror API. * See: * https://blog.sentry.io/2016/01/04/client-javascript-reporting-window-onerror.html * * @param {!Window} win * @param {function(number, boolean):!LogLevel_Enum} levelFunc * @param {string=} opt_suffix */ constructor(win, levelFunc, opt_suffix = '') { /** * In tests we use the main test window instead of the iframe where * the tests runs because only the former is relayed to the console. * @const {!Window} */ this.win = getMode().test && win.__AMP_TEST_IFRAME ? win.parent : win; /** @private @const {function(number, boolean):!LogLevel_Enum} */ this.levelFunc_ = levelFunc; /** @private @const {!LogLevel_Enum} */ this.level_ = this.defaultLevel_(); /** @private @const {string} */ this.suffix_ = opt_suffix; /** @private {?JsonObject} */ this.messages_ = null; this.fetchExternalMessagesOnce_ = once(() => { win .fetch(externalMessagesSimpleTableUrl()) .then((response) => response.json(), noop) .then((opt_messages) => { if (opt_messages) { this.messages_ = /** @type {!JsonObject} */ (opt_messages); } }); }); // This bound assertion function is capable of handling the format used when // error/assertion messages are extracted. This logic hasn't yet been // migrated to an AMP-independent form for use in core. This binding allows // Log assertion helpers to maintain message-extraction capabilities until // that logic can be moved to core. this.boundAssertFn_ = /** @type {!assertions.AssertionFunctionDef} */ ( this.assert.bind(this) ); } /** * @return {!LogLevel_Enum} * @private */ defaultLevel_() { const {win} = this; // No console - can't enable logging. if ( !win.console?.log || // Logging has been explicitly disabled. logHashParam(win) == 0 ) { return LogLevel_Enum.OFF; } // Logging is enabled for tests directly. if (getMode().test && win.ENABLE_LOG) { return LogLevel_Enum.FINE; } // LocalDev by default allows INFO level, unless overriden by `#log`. if (getMode().localDev) { return LogLevel_Enum.INFO; } return this.defaultLevelWithFunc_(); } /** * @param {!Window=} opt_win provided for testing * @return {!LogLevel_Enum} * @private */ defaultLevelWithFunc_(opt_win) { // Delegate to the specific resolver. return this.levelFunc_(logHashParam(opt_win), getMode().development); } /** * @param {string} tag * @param {!LogLevel_Enum} level * @param {!Array} messages * @return {boolean} true if a the message was logged */ msg_(tag, level, messages) { if (level > (levelOverride_ ?? this.level_)) { return false; } const cs = this.win.console; const fn = { [LogLevel_Enum.ERROR]: cs.error, [LogLevel_Enum.INFO]: cs.info, [LogLevel_Enum.WARN]: cs.warn, }[level] ?? cs.log; const args = this.maybeExpandMessageArgs_(messages); // Prefix console message with "[tag]". const prefix = `[${tag}]`; if (isString(args[0])) { // Prepend string to avoid breaking string substitutions e.g. %s. args[0] = prefix + ' ' + args[0]; } else { args.unshift(prefix); } fn.apply(cs, args); return true; } /** * Reports a fine-grained message. * @param {string} tag * @param {...*} args */ fine(tag, ...args) { this.msg_(tag, LogLevel_Enum.FINE, args); } /** * Reports a informational message. * @param {string} tag * @param {...*} args */ info(tag, ...args) { this.msg_(tag, LogLevel_Enum.INFO, args); } /** * Reports a warning message. * @param {string} tag * @param {...*} args */ warn(tag, ...args) { this.msg_(tag, LogLevel_Enum.WARN, args); } /** * Reports an error message. * @param {string} tag * @param {...*} args */ error(tag, ...args) { if (!this.msg_(tag, LogLevel_Enum.ERROR, args)) { const error = this.createError.apply(this, args); error.name = tag || error.name; self.__AMP_REPORT_ERROR?.(error); } } /** * Reports an error message and marks with an expected property. If the * logging is disabled, the error is rethrown asynchronously. * @param {string} tag * @param {...*} args */ expectedError(tag, ...args) { if (!this.msg_(tag, LogLevel_Enum.ERROR, args)) { self.__AMP_REPORT_ERROR?.(this.createExpectedError.apply(this, args)); } } /** * Creates an error object. * @param {...*} var_args * @return {!Error} */ createError(var_args) { return this.setErrorSuffix_(createError.apply(null, arguments)); } /** * Creates an error object with its expected property set to true. * @param {...*} var_args * @return {!Error} */ createExpectedError(var_args) { return this.setErrorSuffix_(createExpectedError.apply(null, arguments)); } /** * @param {!Error} error * @return {!Error} * @private */ setErrorSuffix_(error) { error = duplicateErrorIfNecessary(error); if (this.suffix_) { if (!error.message) { error.message = this.suffix_; } else if (error.message.indexOf(this.suffix_) == -1) { error.message += this.suffix_; } } else if (isUserErrorMessage(error.message)) { error.message = stripUserError(error.message); } return error; } /** * @param {!Array} args * @return {!Array} * @private */ maybeExpandMessageArgs_(args) { return isArray(args[0]) ? this.expandMessageArgs_(/** @type {!Array} */ (args[0])) : args; } /** * Either redirects a pair of (errorId, ...args) to a URL where the full * message is displayed, or displays it from a fetched table. * * This method is used by the output of the `transform-log-methods` babel * plugin. It should not be used directly. Use the (*error|assert*|info|warn) * methods instead. * * @param {!Array} parts * @return {!Array} * @private */ expandMessageArgs_(parts) { // First value should exist. const id = parts.shift(); // Best effort fetch of message template table. // Since this is async, the first few logs might be indirected to a URL even // if in development mode. Message table is ~small so this should be a short // gap. if (getMode(this.win).development) { this.fetchExternalMessagesOnce_(); } return this.messages_?.[id] ? [this.messages_[id]].concat(parts) : [`More info at ${externalMessageUrl(id, parts)}`]; } /** * Throws an error if the first argument isn't trueish. * * Supports argument substitution into the message via %s placeholders. * * Throws an error object that has two extra properties: * - associatedElement: This is the first element provided in the var args. * It can be used for improved display of error messages. * - messageArray: The elements of the substituted message as non-stringified * elements in an array. When e.g. passed to console.error this yields * native displays of things like HTML elements. * * @param {T} shouldBeTrueish The value to assert. The assert fails if it does * not evaluate to true. * @param {!Array|string=} opt_message The assertion message * @param {...*} var_args Arguments substituted into %s in the message. * @return {T} The value of shouldBeTrueish. * @throws {!Error} When `value` is falsey. * @template T * @closurePrimitive {asserts.truthy} */ assert(shouldBeTrueish, opt_message, var_args) { if (isArray(opt_message)) { return this.assert.apply( this, [shouldBeTrueish].concat( this.expandMessageArgs_(/** @type {!Array} */ (opt_message)) ) ); } return assertions.assert.apply( null, [this.suffix_].concat(Array.prototype.slice.call(arguments)) ); } /** * Throws an error if the first argument isn't an Element * * Otherwise see `assert` for usage * * @param {*} shouldBeElement * @param {!Array|string=} opt_message The assertion message * @return {!Element} The value of shouldBeTrueish. * @closurePrimitive {asserts.matchesReturn} */ assertElement(shouldBeElement, opt_message) { return assertions.assertElement( this.boundAssertFn_, shouldBeElement, opt_message ); } /** * Throws an error if the first argument isn't a string. The string can * be empty. * * For more details see `assert`. * * @param {*} shouldBeString * @param {!Array|string=} opt_message The assertion message * @return {string} The string value. Can be an empty string. * @closurePrimitive {asserts.matchesReturn} */ assertString(shouldBeString, opt_message) { return assertions.assertString( this.boundAssertFn_, shouldBeString, opt_message ); } /** * Throws an error if the first argument isn't a number. The allowed values * include `0` and `NaN`. * * For more details see `assert`. * * @param {*} shouldBeNumber * @param {!Array|string=} opt_message The assertion message * @return {number} The number value. The allowed values include `0` * and `NaN`. * @closurePrimitive {asserts.matchesReturn} */ assertNumber(shouldBeNumber, opt_message) { return assertions.assertNumber( this.boundAssertFn_, shouldBeNumber, opt_message ); } /** * Throws an error if the first argument is not an array. * The array can be empty. * * @param {*} shouldBeArray * @param {!Array|string=} opt_message The assertion message * @return {!Array} The array value * @closurePrimitive {asserts.matchesReturn} */ assertArray(shouldBeArray, opt_message) { return assertions.assertArray( this.boundAssertFn_, shouldBeArray, opt_message ); } /** * Throws an error if the first argument isn't a boolean. * * For more details see `assert`. * * @param {*} shouldBeBoolean * @param {!Array|string=} opt_message The assertion message * @return {boolean} The boolean value. * @closurePrimitive {asserts.matchesReturn} */ assertBoolean(shouldBeBoolean, opt_message) { return assertions.assertBoolean( this.boundAssertFn_, shouldBeBoolean, opt_message ); } } /** * Cache for logs. We do not use a Service since the service module depends * on Log and closure literally can't even. * @type {{user: ?Log, dev: ?Log, userForEmbed: ?Log}} */ self.__AMP_LOG = self.__AMP_LOG || { user: null, dev: null, userForEmbed: null, }; const logs = self.__AMP_LOG; /** * Eventually holds a constructor for Log objects. Lazily initialized, so we * can avoid ever referencing the real constructor except in JS binaries * that actually want to include the implementation. * @type {?typeof Log} */ let logConstructor = null; /** * Initializes log constructor. */ export function initLogConstructor() { logConstructor = Log; // Initialize instances for use. If a binary (an extension for example) that // does not call `initLogConstructor` invokes `dev()` or `user()` earlier than // the binary that does call `initLogConstructor` (amp.js), the extension will // throw an error as that extension will never be able to initialize the log // instances and we also don't want it to call `initLogConstructor` either // (since that will cause the Log implementation to be bundled into that // binary). So we must initialize the instances eagerly so that they are ready // for use (stored globally) after the main binary calls `initLogConstructor`. dev(); user(); } /** * Resets log constructor for testing. */ export function resetLogConstructorForTesting() { logConstructor = null; } /** * Calls the log constructor with a given level function and suffix. * @param {function(number, boolean):!LogLevel_Enum} levelFunc * @param {string=} opt_suffix * @return {!Log} */ function callLogConstructor(levelFunc, opt_suffix) { if (!logConstructor) { throw new Error('failed to call initLogConstructor'); } return new logConstructor(self, levelFunc, opt_suffix); } /** * Publisher level log. * * Enabled in the following conditions: * 1. Not disabled using `#log=0`. * 2. Development mode is enabled via `#development=1` or logging is explicitly * enabled via `#log=D` where D >= 1. * 3. AMP.setLogLevel(D) is called, where D >= 1. * * @param {!Element=} opt_element * @return {!Log} */ export function user(opt_element) { // logs.user must exist first to perform the logs.user.win check below if (!logs.user) { logs.user = getUserLogger(USER_ERROR_SENTINEL); } if (isFromEmbed(logs.user.win, opt_element)) { return ( logs.userForEmbed || (logs.userForEmbed = getUserLogger(USER_ERROR_EMBED_SENTINEL)) ); } return logs.user; } /** * Getter for user logger * @param {string=} suffix * @return {!Log} */ function getUserLogger(suffix) { return callLogConstructor( (logNum, development) => development || logNum >= 1 ? LogLevel_Enum.FINE : LogLevel_Enum.WARN, suffix ); } /** * AMP development log. Calls to `devLog().assert` and `dev.fine` are stripped * in the PROD binary. However, `devLog().assert` result is preserved in either * case. * * Enabled in the following conditions: * 1. Not disabled using `#log=0`. * 2. Logging is explicitly enabled via `#log=D`, where D >= 2. * 3. AMP.setLogLevel(D) is called, where D >= 2. * * @return {!Log} */ export function dev() { return ( logs.dev || (logs.dev = callLogConstructor((logNum) => logNum >= 3 ? LogLevel_Enum.FINE : logNum >= 2 ? LogLevel_Enum.INFO : LogLevel_Enum.OFF )) ); } /** * @param {!Window} win * @param {!Element=} opt_element * @return {boolean} isEmbed */ function isFromEmbed(win, opt_element) { return opt_element && opt_element.ownerDocument.defaultView != win; } /** * Throws an error if the first argument isn't trueish. * * Supports argument substitution into the message via %s placeholders. * * Throws an error object that has two extra properties: * - associatedElement: This is the first element provided in the var args. * It can be used for improved display of error messages. * - messageArray: The elements of the substituted message as non-stringified * elements in an array. When e.g. passed to console.error this yields * native displays of things like HTML elements. * * @param {T} shouldBeTrueish The value to assert. The assert fails if it does * not evaluate to true. * @param {!Array|string=} opt_message The assertion message * @param {*=} opt_1 Optional argument (Var arg as individual params for better * @param {*=} opt_2 Optional argument inlining) * @param {*=} opt_3 Optional argument * @param {*=} opt_4 Optional argument * @param {*=} opt_5 Optional argument * @param {*=} opt_6 Optional argument * @param {*=} opt_7 Optional argument * @param {*=} opt_8 Optional argument * @param {*=} opt_9 Optional argument * @return {T} The value of shouldBeTrueish. * @throws {!Error} When `shouldBeTrueish` is falsey. * @template T * @closurePrimitive {asserts.truthy} */ export function devAssert( shouldBeTrueish, opt_message, opt_1, opt_2, opt_3, opt_4, opt_5, opt_6, opt_7, opt_8, opt_9 ) { if (mode.isMinified()) { return shouldBeTrueish; } if (self.__AMP_ASSERTION_CHECK) { // This will never execute regardless, but will be included on unminified // builds. It will be DCE'd away from minified builds, and so can be used to // validate that Babel is properly removing dev assertions in minified // builds. console /*OK*/ .log('__devAssert_sentinel__'); } return dev()./*Orig call*/ assert( shouldBeTrueish, opt_message, opt_1, opt_2, opt_3, opt_4, opt_5, opt_6, opt_7, opt_8, opt_9 ); } /** * Throws an error if the first argument isn't trueish. * * Supports argument substitution into the message via %s placeholders. * * Throws an error object that has two extra properties: * - associatedElement: This is the first element provided in the var args. * It can be used for improved display of error messages. * - messageArray: The elements of the substituted message as non-stringified * elements in an array. When e.g. passed to console.error this yields * native displays of things like HTML elements. * * @param {T} shouldBeTrueish The value to assert. The assert fails if it does * not evaluate to true. * @param {!Array|string=} opt_message The assertion message * @param {*=} opt_1 Optional argument (Var arg as individual params for better * @param {*=} opt_2 Optional argument inlining) * @param {*=} opt_3 Optional argument * @param {*=} opt_4 Optional argument * @param {*=} opt_5 Optional argument * @param {*=} opt_6 Optional argument * @param {*=} opt_7 Optional argument * @param {*=} opt_8 Optional argument * @param {*=} opt_9 Optional argument * @return {T} The value of shouldBeTrueish. * @throws {!Error} When `shouldBeTrueish` is falsey. * @template T * @closurePrimitive {asserts.truthy} */ export function userAssert( shouldBeTrueish, opt_message, opt_1, opt_2, opt_3, opt_4, opt_5, opt_6, opt_7, opt_8, opt_9 ) { return user()./*Orig call*/ assert( shouldBeTrueish, opt_message, opt_1, opt_2, opt_3, opt_4, opt_5, opt_6, opt_7, opt_8, opt_9 ); }