diff options
Diffstat (limited to 'shared/metrics-8/node_modules/@amp-metrics/mt-metricskit-utils-private/dist/mt-metricskit-utils-private.esm.js')
| -rw-r--r-- | shared/metrics-8/node_modules/@amp-metrics/mt-metricskit-utils-private/dist/mt-metricskit-utils-private.esm.js | 2428 |
1 files changed, 2428 insertions, 0 deletions
diff --git a/shared/metrics-8/node_modules/@amp-metrics/mt-metricskit-utils-private/dist/mt-metricskit-utils-private.esm.js b/shared/metrics-8/node_modules/@amp-metrics/mt-metricskit-utils-private/dist/mt-metricskit-utils-private.esm.js new file mode 100644 index 0000000..435731d --- /dev/null +++ b/shared/metrics-8/node_modules/@amp-metrics/mt-metricskit-utils-private/dist/mt-metricskit-utils-private.esm.js @@ -0,0 +1,2428 @@ +/* + * src/reflect.js + * mt-metricskit-utils-private + * + * Copyright © 2015-2017 Apple Inc. All rights reserved. + * + */ + +/** + ************************************ PRIVATE METHODS/IVARS ************************************ + */ +var _nonOverrideableFunctions = { setDelegate: true }; + +/** + ************************************ PSEUDO-PRIVATE METHODS/IVARS ************************************ + * These functions need to be accessible for ease of testing, but should not be used by clients + */ +function _utResetNonOverridableFunctions() { + _nonOverrideableFunctions = { setDelegate: true }; +} + +/** + ************************************ PUBLIC METHODS/IVARS ************************************ + */ + +/** + * Simple shallow clone which just copies over top-level keys and values (without "hasOwnProperty" checks). + * Useful for using a passed-in map without having that parameter data be corrupted by the function it's passed to. + * @param source + * @returns {object} will never return null... worst case will return an empty object. + */ +function shallowClone(source) { + var dest = {}; + var sourceHasGetterAndSetterMethods = hasGetterAndSetterMethods(source); + var aGetter; + var aSetter; + + for (var key in source) { + aGetter = null; + aSetter = null; + + if (sourceHasGetterAndSetterMethods) { + // Be careful to copy aGetter and setter methods properly, per: http://ejohn.org/blog/javascript-getters-and-setters/ + aGetter = source.__lookupGetter__(key); + aSetter = source.__lookupSetter__(key); + } + + if (aGetter || aSetter) { + if (aGetter) { + dest.__defineGetter__(key, aGetter); + } + if (aSetter) { + dest.__defineSetter__(key, aSetter); + } + } else { + dest[key] = source[key]; + } + } + return dest; +} + +/** + * Returns true if the specified obj is not undefined + * NOTE: Does not work for global variables (because the variable gets defined by virtue of passing it in)... use "typeof()" directly + */ +function isDefined(anObject) { + return typeof anObject !== 'undefined'; +} + +/** + * Returns true if the specified obj is not undefined and not null + * NOTE: Does not work for global variables... in that case, use "typeof()" directly, because the act of passing them to here will make them appear to be defined + */ +function isDefinedNonNull(anObject) { + return isDefined(anObject) && anObject !== null; +} + +/** + * Returns true if the specified obj is not undefined and not null and not empty + * NOTE: Does not work for global variables... in that case, use "typeof()" directly, because the act of passing them to here will make them appear to be defined + */ +function isDefinedNonNullNonEmpty(anObject) { + return ( + isDefinedNonNull(anObject) && !isEmptyString(anObject) && !isEmptyArray(anObject) && !isEmptyObject(anObject) + ); +} + +function isEmptyString(anObject) { + return isString(anObject) && anObject.length === 0; +} + +function isEmptyArray(anObject) { + return isArray(anObject) && anObject.length === 0; +} + +function isEmptyObject(anObject) { + return isObject(anObject) && Object.keys(anObject).length === 0; +} + +function isFunction(anObject) { + // the following works for regular functions and native functions, e.g. iTunes.buy + return typeof anObject === 'function'; +} + +function isNumber(anObject) { + return typeof anObject == 'number'; +} + +function isInteger(anObject) { + return isNumber(anObject) && anObject % 1 === 0; +} + +function isString(anObject) { + return typeof anObject === 'string' || anObject instanceof String; +} + +function isElement(anObject) { + return !!anObject && anObject.nodeType == 1; +} + +function isArray(anObject) { + return !!anObject && anObject.constructor === Array; +} + +function isObject(anObject) { + return !!anObject && anObject.constructor === Object; +} + +/* + * NOTE: this method skips object properties that are functions. + */ +function values(anObject) { + var values = []; + for (var property in anObject) { + var aValue = anObject[property]; + if (anObject.hasOwnProperty(property) && !isFunction(aValue)) { + values.push(aValue); + } + } + return values; +} + +function keys(anObject) { + var keys = []; + for (var property in anObject) { + if (anObject.hasOwnProperty(property) && !isFunction(anObject[property])) { + keys.push(property); + } + } + return keys; +} + +/** + * Returns "true" if the passed-in object has any values at all on it. + * NOTE: this method skips object properties that are functions. + */ +function hasAnyKeys(anObject) { + for (var aKey in anObject) { + if (anObject.hasOwnProperty(aKey)) { + return true; + } + } +} + +/** + * Returns "true" if the passed-in object has any values on it at all *and* at least one of those values is non-null. + * NOTE: this method skips object properties that are functions. + */ +function hasAnyNonNullKeys(anObject) { + for (var aKey in anObject) { + if (anObject.hasOwnProperty(aKey) && anObject[aKey]) { + return true; + } + } +} + +/** + * @return {boolean} true if the object has the default object getter and setter methods (e.g. __lookupGetter__()) + * see <rdar://problem/33045481> MetricsKit: Protect against JS errors when __lookupGetter__ is not on the object prototype + */ +function hasGetterAndSetterMethods(anObject) { + return ( + isObject(anObject) && + isFunction(anObject.__lookupGetter__) && + isFunction(anObject.__lookupSetter__) && + isFunction(anObject.__defineGetter__) && + isFunction(anObject.__defineSetter__) + ); +} + +/* + * NOTE: this method returns *only* object properties that are functions. + */ +function methods(anObject) { + var methods = []; + for (var property in anObject) { + var aValue = anObject[property]; + if (anObject.hasOwnProperty(property) && isFunction(aValue)) { + methods.push(aValue); + } + } + return methods; +} + +/* + * NOTE: this method skips object properties that are functions. + */ +function invert(anObject) { + var invertedMap = {}; + for (var property in anObject) { + if (anObject.hasOwnProperty(property) && !isFunction(anObject[property])) { + invertedMap[anObject[property]] = property; + } + } + return invertedMap; +} + +/** + * Adds all the fields of the objects in the varargs to the fields in the first parameter, "obj". + * *All* "hasOwnProperty" fields will be added, including functions and fields with no values. + * @param {Object} targetObject an object with keys and values. If only one parameter is provided, the return value will be the non-null values of that single object. + * @param {varargs} sourceObjectN a variable number of Object arguments from 0-N. Each object's fields will be copied into targetObject. Later objects take precedence over earlier ones. + * @return targetObject (*not* a clone) with the additional fields added.. + */ +function extend(targetObject /* , ...sourceObjectN(varargs) */) { + var argumentsArray = [true, true, true].concat(Array.prototype.slice.call(arguments)); + return copyKeysAndValues.apply(null, argumentsArray); +} + +/** + * Takes one or more objects, [possibly] cleans them (removes keys that are typeof 'function', keys with 'null' values, keys with 'undefined' values), + * merges them (later objects take precedence), and returns a single object with the union of all remaining fields. + * @param keepNullsAndUndefined a boolean if true fields with a "null" or "undefined" value will still be copied. + * Otherwise, if false, any field with a "null" or "undefined" value will not be copied. + * @param keepFunctions a boolean if true fields whose value is typeof 'function' will still be copied. + * Otherwise, if false, any field with a typeof 'function' value will not be copied. + * @param inPlace a boolean if true will copy all results to the "targetObject" object, rather than copying them all to a new object. + * Otherwise if "inPlace" is false, a new object will be returned and all passed in objects are treated as immutable and so will never be modified. + * @param {Object} targetObject an object with keys and values. If only one parameter is provided, the return value will be the non-null values of that single object. + * @param {varargs} sourceObjectN a variable number of Object arguments from 0-N. Each object's fields will be copied into targetObject. Later objects take precedence over earlier ones. + * @return either targetObject (if "inPlace" is true) or a new object (if "inPlace" is false or "targetObject" is null) with the + * union of all fields, filtered based on the values of the keepNullsAndUndefined and keepFunctions parameters + * @example copyKeysAndValues(true, {}) ===> {} + * @example copyKeysAndValues(true, null) ===> {} + * @example copyKeysAndValues({true, "foo":10}) ===> {"foo":10} + * @example copyKeysAndValues({true, "foo":10, "bar":null}) ===> {"foo":10} + * @example copyKeysAndValues({false, "foo":10, "bar":null}) ===> {"foo":10, "bar":null} + * @example copyKeysAndValues({true, "foo":10, "bar":null}, {"cat":null}) ===> {"foo":10} + * @example copyKeysAndValues({true, "foo":10, "bar":null}, {"cat":null, "mouse":"gray"}) ===> {"foo":10, "mouse":"gray"} + * @example copyKeysAndValues({true, "foo":10, "bar":null}, {"cat":null, "mouse":"gray", "dog":"bark"}) ===> {"foo":10, "mouse":"gray", "dog":"bark"} + * @example copyKeysAndValues({true, "foo":10, "bar":null}, {"cat":null, "mouse":"gray", "dog":"bark", "foo":11}) ===> {"foo":11, "mouse":"gray", "dog":"bark"} + * @example copyKeysAndValues({true, "foo":10, "bar":null}, {"cat":null, "mouse":"gray", "dog":"bark", "foo":11}, {"foo":12}) ===> {"foo":12, "mouse":"gray", "dog":"bark"} + */ +function copyKeysAndValues(keepNullsAndUndefined, keepFunctions, inPlace, targetObject /*, sourceObjectN(varargs)*/) { + var returnValue = inPlace ? targetObject || {} : {}; + var sourceObject; + + for (var ii = 3; ii < arguments.length; ii++) { + sourceObject = arguments[ii]; + + for (var key in sourceObject) { + if (Object.prototype.hasOwnProperty.call(sourceObject, key)) { + var value = sourceObject[key]; + + if (keepNullsAndUndefined || (value !== null && value !== undefined)) { + if (keepFunctions || typeof value !== 'function') { + returnValue[key] = value; + } + } + } + } + } + return returnValue; +} + +/** + * Add one or more function names to the list of non overrideable functions + * attachDelegate will check this list and not override any functions with matching names + * @param {Array} functionNames + * @returns undefined + */ +function addNonOverrideableFunctions(functionNames) { + for (var i = 0; i < functionNames.length; i++) { + var functionName = functionNames[i]; + _nonOverrideableFunctions[functionName] = true; + } +} + +/** + * Replace any "target" methods found on "delegate" with the delegate's version of the method. + * The replacement function will actually be our own wrapper function with the original function attached as a property called origFunction + * in case the delegate's replacement method wants to, essentially, call "super" + * We do delegation this way, vs. checking each time a "target" function is called, because this way we don't pollute the implementation + * of all the target's functions. + * Subsequent calls to "attachDelegate" will then replace whatever methods *they* match, including methods that have already been replaced. + * This allows callers to use "canned" delegates to get most of their functionality, but still replace some number of methods that need custom implementations. + * If a replaced method is overridden again with a subsequent "setDelegate" call, the "origFunction" parameter will be the previous delegate's function. + * NOTE: Only methods present on "target" will be replaced + * @param {Object} target + * @param {Object} delegate + * @param {Boolean} useOriginalContext whether to use the execution context of the original method for the delegate method. Default to false + * @returns {Boolean} true if one or more methods on the delegate object were attached to the target object + */ +function attachDelegate(target, delegate, useOriginalContext) { + var returnValue = false; + useOriginalContext = useOriginalContext || false; + + if (target && delegate && target !== delegate) { + // only attach methods that exist on the target + var methodsToOmitMap = {}; + Object.keys(delegate).forEach(function (methodName) { + if (!target[methodName]) { + methodsToOmitMap[methodName] = true; + } + }); + + // ignore nonOverrideableFunctions + extend(methodsToOmitMap, _nonOverrideableFunctions); + + returnValue = attachMethods(target, delegate, useOriginalContext ? target : null, methodsToOmitMap); + } + + return returnValue; +} + +/** + * Replace any "target" methods found on "source" with the source's version of the method. + * The replacement function will actually be our own wrapper function with the original function attached as a property called origFunction + * in case the source's replacement method wants to, essentially, call "super" + * We do delegation this way, vs. checking each time a "target" function is called, because this way we don't pollute the implementation + * of all the target's functions. + * Subsequent calls to "attachMethods" will then replace whatever methods *they* match, including methods that have already been replaced. + * This allows callers to use "canned" sources to get most of their functionality, but still replace some number of methods that need custom implementations. + * If a replaced method is overridden again with a subsequent "attachMethod" call, the "origFunction" parameter will be the previous source's function. + * @param {Object} target + * @param {Object} source + * @param {Object} (optional) methodContext 'this' context to apply to any bound methods; defaults to the source object + * @param {Object} (optional) methodsToOmitMap a dictionary of method names to ignore when copying + * @returns {Boolean} true if one or more methods on the source object were attached to the target object + */ +function attachMethods(target, source, methodContext, methodsToOmitMap) { + var returnValue = false; + + if (target && source) { + methodsToOmitMap = methodsToOmitMap || {}; + methodContext = methodContext || source; + + var captureFunction = function captureFunction( + methodContext, + capturedOrigFunction, + source, + origFunctionContext, + functionName + ) { + var returnValue = function () { + // dereference the source so delegate chaining (source object delegating to another object) works properly + return source[functionName].apply(methodContext, arguments); + }; + // Attach the origFunction in case its needed later by the caller for delegate chaining. + // Do that in here too, so it doesn't get overwritten in the loop... + if (capturedOrigFunction) { + returnValue.origFunction = capturedOrigFunction; + } + // we need to add a symbol to the attached function that doesn't have an original function, in order to remove them by "detachMethods()". + returnValue.attachedMethod = true; + + return returnValue; + }; + + for (var functionName in source) { + if (!(functionName in methodsToOmitMap)) { + if (source[functionName] && isFunction(source[functionName])) { + var origFunction = target[functionName]; + var origFunctionExists = origFunction && isFunction(origFunction); + var capturedOrigFunction = null; + if (origFunctionExists) { + // Avoid binding the delegate function repeatedly + // We need to avoid binding the delegated function in order to the "detachMethods" function could reset the delegated function to the original function by the "origFunction" chain. + // And the delegated function's context was already bound in captureFunction. + if (origFunction.attachedMethod === true) { + capturedOrigFunction = origFunction; + } else { + capturedOrigFunction = origFunction.bind(target); + } + } + + // Careful! This is that tough "closure in a loop" case where the local variables captured by the closure + // will change even for previously created closure functions on each iteration of this loop! + // So, we need to capture it in an additional, invoked, closure... + // See (and cited links): http://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example + target[functionName] = captureFunction( + methodContext, + capturedOrigFunction, + source, + target, + functionName + ); + returnValue = true; + } + } + } + } + return returnValue; +} + +/** + * detach the any of delegated methods(the functions which have 'attachedMethod' symbol) from the provided object + * @param target + * @returns {boolean} true if one or more delegate methods were detached from the target object + */ +function detachMethods(target) { + var returnValue = false; + for (var propKey in target) { + if (isFunction(target[propKey]) && target[propKey].attachedMethod === true) { + var methodIsDelegated = target[propKey].origFunction; + if (methodIsDelegated) { + while (target[propKey].origFunction) { + target[propKey] = target[propKey].origFunction; + returnValue = true; + } + } else { + delete target[propKey]; + } + } + } + return returnValue; +} + +/** + * Convenience method to loop over a set of delegatable targets and apply appropriate delegates + * @param {Object} targetMap containing one or more delegatable target objects + * @param {Object} delegateMap containing one or more delegates + * @return {Object} contains the results of all setDelegate calls + * @example usage: + * var targetMap = { environment: environmentObject, eventRecorder: eventRecorderObject, foo: fooObject }; + * var delegateMap = { environment: environmentDelegate, eventRecorder: eventRecorderDelegate }; + * setDelegates(targetMap, delegateMap); // returns { environment: true, eventRecorder: true } + * // targetMap.environment's delegate is now environmentDelegate and targetMap.eventRecorder's delegate is now eventRecorderDelegate + */ +function setDelegates(targetMap, delegateMap) { + var resultObject = {}; + + for (var targetName in targetMap) { + if (delegateMap[targetName] && isFunction(targetMap[targetName].setDelegate)) { + resultObject[targetName] = targetMap[targetName].setDelegate.apply(targetMap[targetName], [delegateMap[targetName]].concat(Array.prototype.slice.call(arguments, 2))); + } + } + + return resultObject; +} + +/** + * Reset the delegate functions from the objects of provided target map. + * @param {Object} targetMap containing one or more delegatable target objects + * @returns {Boolean} true if one or more delegate methods were reset from the target object + */ +var resetDelegates = function resetDelegates(targetMap) { + var returnValue = false; + + for (var targetName in targetMap) { + var delegateObject = targetMap[targetName]; + if (delegateObject && typeof delegateObject === 'object' && isFunction(delegateObject.setDelegate)) { + returnValue |= detachMethods(delegateObject); + } + } + return !!returnValue; +}; + +/** + * @param {Object} sourceObject + * @param {Object} targetObject + * @returns {Boolean} true if one or more methods on the target was delegated, false otherwise + * If one (or more) of the source object's methods has been delegated to a function, + * the same method on the target object will also be delegated to that function + */ +function copyDelegatedFunctions(sourceObject, targetObject) { + var returnValue = null; + + if (sourceObject && targetObject && targetObject.setDelegate) { + var delegatedFunctions = {}; + var functionName; + + for (functionName in sourceObject) { + if (isFunction(sourceObject[functionName]) && sourceObject[functionName].origFunction) { + delegatedFunctions[functionName] = sourceObject[functionName]; + } + } + + returnValue = targetObject.setDelegate(delegatedFunctions); + } + + return returnValue; +} + +/** + * Returns a deduped array (similar to a Set object) using the contents from both arrayA and arrayB + * @param {Array} arrayA the first array to dedupe + * @param {Array} arrayB the other array to dedupe + * @return {Array} The deduped array + */ +function dedupedArray(arrayA, arrayB) { + var tempDict = {}; // necessary for returning array of unique values and not having access to Set object in ES5 + if (arrayA) { + for (var i = 0; i < arrayA.length; i++) { + tempDict[arrayA[i]] = 0; // value for key doesn't matter; we want a list of unique values + } + } + if (arrayB) { + for (var j = 0; j < arrayB.length; j++) { + tempDict[arrayB[j]] = 0; // value for key doesn't matter; we want a list of unique values + } + } + return Object.keys(tempDict); // equivalent to a Set of unique values +} + +/** + * Returns the global scope object associated with the platform + */ +function globalScope() { + if (typeof globalThis !== 'undefined') { + return globalThis; + } else if (typeof global !== 'undefined') { + return global; + } else if (typeof window !== 'undefined') { + return window; + } else { + return {}; + } +} + +var reflect = /*#__PURE__*/Object.freeze({ + __proto__: null, + _utResetNonOverridableFunctions: _utResetNonOverridableFunctions, + shallowClone: shallowClone, + isDefined: isDefined, + isDefinedNonNull: isDefinedNonNull, + isDefinedNonNullNonEmpty: isDefinedNonNullNonEmpty, + isEmptyString: isEmptyString, + isEmptyArray: isEmptyArray, + isEmptyObject: isEmptyObject, + isFunction: isFunction, + isNumber: isNumber, + isInteger: isInteger, + isString: isString, + isElement: isElement, + isArray: isArray, + isObject: isObject, + values: values, + keys: keys, + hasAnyKeys: hasAnyKeys, + hasAnyNonNullKeys: hasAnyNonNullKeys, + hasGetterAndSetterMethods: hasGetterAndSetterMethods, + methods: methods, + invert: invert, + extend: extend, + copyKeysAndValues: copyKeysAndValues, + addNonOverrideableFunctions: addNonOverrideableFunctions, + attachMethods: attachMethods, + detachMethods: detachMethods, + attachDelegate: attachDelegate, + setDelegates: setDelegates, + resetDelegates: resetDelegates, + copyDelegatedFunctions: copyDelegatedFunctions, + dedupedArray: dedupedArray, + globalScope: globalScope +}); + +/* + * src/backoff.js + * mt-metricskit-utils-private + * + * Copyright © 2015-2017 Apple Inc. All rights reserved. + * + */ + +var DEFAULTS = { + exponential: { + maxWait: 1500, + initialDelay: 100, + factor: 2 + } +}; + +/** + * @param {int} (optional) initialDelay - time in ms before the first reattempt (each subsequent reattempt will wait exponentially longer than the previous one) + * @param {int} (optional) maxWait - max cumulative time in ms to wait before giving up (does not include the time taken by the function to execute) + setting this to 0 will cause the strategy to retry indefinitely + * @param {int} (optional) factor - multiplier to apply when delaying subsequent reattempts. Defaults to DEFAULT_EXPONENT_FACTOR + */ +var ExponentialStrategy = function (initialDelay, maxWait, factor) { + this.delay = initialDelay || DEFAULTS.exponential.initialDelay; + this.maxWait = isNumber(maxWait) ? maxWait : DEFAULTS.exponential.maxWait; + this.factor = factor || DEFAULTS.exponential.factor; + + this.timeWaited = 0; +}; + +ExponentialStrategy.prototype.nextDelay = function nextDelay() { + var returnValue = null; + + var timeRemaining = this.maxWait - this.timeWaited; + + if (timeRemaining > 0) { + this.delay = Math.min(this.delay, timeRemaining); + this.timeWaited += this.delay; + } + + if (this.maxWait === 0 || timeRemaining > 0) { + returnValue = this.delay; + this.delay = this.delay * this.factor; // increase the delay for next time + } + + return returnValue; +}; + +/** + * Execute a function according to a given backoff failure strategy + * @param {Object} strategy is an object representing a failure stategy. It has a nextDelay() method that returns the time in ms to wait until reattempting + * @param {Function} networkRequestor - the function to execute. It should accept an onSuccessHandler and an onFailureHandler as its final arguments + * @param {Function} onSuccessHandler - callback to execute on success + * @param {Function} onFailureHandler - callback to execute on failure + */ +function _backoff(strategy, networkRequestor, onSuccessHandler, onFailureHandler) { + var onBackoff = function onBackoff() { + var delay = strategy.nextDelay(); + if (delay) { + setTimeout(_backoff.bind(null, strategy, networkRequestor, onSuccessHandler, onFailureHandler), delay); + } else { + onFailureHandler.apply(onFailureHandler, arguments); + } + }; + + networkRequestor.call(networkRequestor, onSuccessHandler, onBackoff); +} + +/** + * Execute a function according to an exponential backoff failure strategy + * @param {Function} networkRequestor - the function to execute. It should accept an onSuccessHandler and an onFailureHandler as its final arguments + * @param {Function} onSuccessHandler - callback to execute on success + * @param {Function} onFailureHandler - callback to execute on failure + * @param {int} (optional) initialDelay - time in ms before the first reattempt (each subsequent reattempt will wait exponentially longer than the previous one) + * @param {int} (optional) maxWait - max cumulative time in ms to wait before giving up (does not include the time taken by the function to execute) + setting this to 0 will cause the strategy to retry indefinitely + * @param {int} (optional) factor - multiplier to apply when delaying subsequent reattempts. Defaults to DEFAULT_EXPONENT_FACTOR + */ +function exponentialBackoff(networkRequestor, onSuccessHandler, onFailureHandler, initialDelay, maxWait, factor) { + var strategy = new ExponentialStrategy(initialDelay, maxWait, factor); + _backoff(strategy, networkRequestor, onSuccessHandler, onFailureHandler); +} + +var backoff = /*#__PURE__*/Object.freeze({ + __proto__: null, + exponentialBackoff: exponentialBackoff +}); + +/* + * src/number.js + * mt-metricskit-utils-private + * + * Copyright © 2015-2017 Apple Inc. All rights reserved. + * + */ + +/** + ************************************ PUBLIC METHODS/IVARS ************************************ + */ + +/** + * "De-res" a number (lower the resolution of the number) per the Privacy Team and these radars: + * <rdar://problem/17423020> Add "capacityXXX" fields to UserXP Figaro reporting. + * <rdar://problem/23571925> Privacy: De-res capacityXXX fields + * Default behavior will de-res numbers by a magnitude of 1024^2 ie. bytes to megabytes and remove the last two significant digits + * For example, a raw number of bytes "de-res"'d to MB, but without the "floor" filter, would look like these examples: + * 31708938240/1024/1024 ==> 30240 + * 15854469120/1024/1024 ==> 15120 + * 63417876480/1024/1024 ==> 60480 + * + * With the "floor" formula we replace the two least significant digits with "00" + * Doing so will convert values like so: + * + * 31708938240/1024/1024 ==> 30200 + * 15854469120/1024/1024 ==> 15100 + * 63417876480/1024/1024 ==> 60400 + * + * @param {number} aNumber + * @param {number} (optional) magnitude, must be greater than 0. default 1024^2 + * @param {number} (optional) significantDigits to remove, must be a positive integer or 0. default 2 + * @returns {number} if the "aNumber" parameter is absent, the return value will be undefined. + * If any of the arguments are disallowed values, the value "NaN" will be returned. + * @overridable + */ +function deResNumber(aNumber, magnitude, significantDigits) { + var returnValue = undefined; + + if (isDefined(aNumber)) { + if (!isDefined(magnitude)) { + magnitude = 1024 * 1024; + } + if (!isDefined(significantDigits)) { + significantDigits = 2; + } + + if ( + isNumber(aNumber) && + isNumber(magnitude) && + magnitude > 0 && + isInteger(significantDigits) && + significantDigits >= 0 + ) { + var roundFactor = Math.pow(10, significantDigits); + var roundOperation = aNumber > 0 ? 'floor' : 'ceil'; + + returnValue = Math[roundOperation](aNumber / magnitude / roundFactor) * roundFactor; + } else { + returnValue = NaN; + } + } + + return returnValue; +} + +var number = /*#__PURE__*/Object.freeze({ + __proto__: null, + deResNumber: deResNumber +}); + +/* + * src/config.js + * mt-metricskit-utils-private + * + * Copyright © 2015-2017 Apple Inc. All rights reserved. + * + */ + +/** + * @deprecated This class will be removed in the next Major release because the methods of this class have been moved to the mt-client-config/src/config/metrics_config.js + * Config utility methods + * IMPORTANT: These methods should be called within the context of a Config instance that implements a value() method, + * such as @amp/mt-client-config (the singleton or any instance created by it). + * They can be attached via reflect.attachMethods, passing the config instance as the methodContext + * @example: + * var Config = require('@amp/mt-client-config'); + * var configUtils = ( ... ); // this file + * reflect.attachMethods(Config, configUtils, Config); + * Failing to use the correct context will result in thrown errors ("this.value is not a function"). + */ + +/** + ************************************ PUBLIC METHODS/IVARS ************************************ + */ + +/** + * @deprecated + * Boolean config value which, when "true", tells clients to avoid all metrics code paths (different than simply not sending metrics). + * Useful for avoiding discovered client bugs. + * NOTE1: This will cause unrecoverable event loss, as the clients will not be recording events at all. + * NOTE2: Typically all event_handlers will check for this in addition to "recordEvent()" checking because that way + * if a client overrides "recordEvent", these checks will still take effect. + * We also test it in recordEvent() in case someone creates their own event_handler, we'd still want to exclude what needs to be excluded. + * @param {String} (optional) topic the Figaro topic to use to look up config values + * @returns {boolean} + */ +function disabled(topic) { + return this.value('disabled', topic) ? true : false; +} + +/** + * @deprecated - Deprecated Language, use denylist instead + * Array config value which instructs clients to avoid sending particular event types. + * Useful for reducing server processing in emergencies by abandoning less-critical events. + * Useful for dealing with urgent privacy concerns, etc., around specific events. + * NOTE1: This will cause unrecoverable event loss, as the clients will not be recording events at all. + * NOTE2: Typically all event_handlers will check for this in addition to "recordEvent()" checking because that way + * if a client overrides "recordEvent", these checks will still take effect. + * We also test it in recordEvent() in case someone creates their own event_handler, we'd still want to exclude what needs to be excluded, in case they don't. + * @param {String} (optional) topic the Figaro topic to use to look up config values + * @returns {Array} Guaranteed to always return a valid array, though it may be empty if the value was unset in config + */ +function blacklistedEvents(topic) { + return denylistedEvents.call(this, topic); +} + +/** + * @deprecated + * Array config value which instructs clients to avoid sending particular event types. + * Useful for reducing server processing in emergencies by abandoning less-critical events. + * Useful for dealing with urgent privacy concerns, etc., around specific events. + * NOTE1: This will cause unrecoverable event loss, as the clients will not be recording events at all. + * NOTE2: Typically all event_handlers will check for this in addition to "recordEvent()" checking because that way + * NOTE3: To honor both old blacklistedEvents configs and new denylistedEvents Configs, we'll merge the blacklistedEvents config with the denylistedEvents config. + * if a client overrides "recordEvent", these checks will still take effect. + * We also test it in recordEvent() in case someone creates their own event_handler, we'd still want to exclude what needs to be excluded, in case they don't. + * @param {String} (optional) topic the Figaro topic to use to look up config values + * @returns {Array} Guaranteed to always return a valid array, though it may be empty if the value was unset in config + */ +function denylistedEvents(topic) { + var denylistedEventsArray = this.value('denylistedEvents', topic); + var blacklistedEventsArray = this.value('blacklistedEvents', topic); + return dedupedArray(blacklistedEventsArray, denylistedEventsArray); +} + +/** + * @deprecated + * Array config value which instructs clients to avoid sending particular event fields. + * Useful for dealing with urgent privacy concerns, etc., around specific event fields (e.g. dsid) + * NOTE: Typically all event_handlers will check for this in addition to "recordEvent()" checking because that way + * if a client overrides "recordEvent", these checks will still take effect. + * We also test it in recordEvent() in case someone creates their own event_handler, we'd still want to exclude what needs to be excluded, in case they don't. + * @param {String} (optional) topic the Figaro topic to use to look up config values + * @returns {Array} Guaranteed to always return a valid array, though it may be empty if the value was unset in config + */ +function blacklistedFields(topic) { + return denylistedFields.call(this, topic); +} + +/** + * @deprecated + * Array config value which instructs clients to avoid sending particular event fields. + * Useful for dealing with urgent privacy concerns, etc., around specific event fields (e.g. dsid) + * NOTE: Typically all event_handlers will check for this in addition to "recordEvent()" checking because that way + * NOTE2: To honor both old blacklistedFields configs and new denylistedFields configs, we'll merge the blacklistedEvents config with the denylistedEvents config. + * if a client overrides "recordEvent", these checks will still take effect. + * We also test it in recordEvent() in case someone creates their own event_handler, we'd still want to exclude what needs to be excluded, in case they don't. + * @param {String} (optional) topic the Figaro topic to use to look up config values + * @returns {Array} Guaranteed to always return a valid array, though it may be empty if the value was unset in config + */ +function denylistedFields(topic) { + var denylistedFieldsArray = this.value('denylistedFields', topic); + var blacklistedFieldsArray = this.value('blacklistedFields', topic); + return dedupedArray(blacklistedFieldsArray, denylistedFieldsArray); +} + +/** + * @deprecated + * Remove all blacklisted fields from the passed-in object. + * IMPORTANT: This action is performed in-place for performance of not having to create new objects each time. + * NOTE: Typically all event_handlers will call this in addition to "recordEvent()" calling it because that way + * if a client overrides "recordEvent", these checks will still take effect. + * We also test it in recordEvent() in case someone creates their own event_handler, we'd still want to exclude what needs to be excluded, in case they don't. + * @param {Object} eventFields a dictionary of event data + * @param {String} (optional) topic the Figaro topic to use to look up config values + * @returns {Object} the passed-in object with any blacklisted fields removed + */ +function removeBlacklistedFields(eventFields, topic) { + return removeDenylistedFields.call(this, eventFields, topic); +} + +/** + * @deprecated + * Remove all denylisted fields from the passed-in object. + * IMPORTANT: This action is performed in-place for performance of not having to create new objects each time. + * NOTE: Typically all event_handlers will call this in addition to "recordEvent()" calling it because that way + * if a client overrides "recordEvent", these checks will still take effect. + * We also test it in recordEvent() in case someone creates their own event_handler, we'd still want to exclude what needs to be excluded, in case they don't. + * @param {Object} eventFields a dictionary of event data + * @param {String} (optional) topic the Figaro topic to use to look up config values + * @returns {Object} the passed-in object with any denylisted fields removed + */ +function removeDenylistedFields(eventFields, topic) { + if (eventFields) { + var denylistedFieldsArray = denylistedFields.call(this, topic); + + for (var ii = 0; ii < denylistedFieldsArray.length; ii++) { + var aDenylistedField = denylistedFieldsArray[ii]; + // Double check this is not null (or empty string), or "delete" will blow up... + if (aDenylistedField) { + if (aDenylistedField in eventFields) { + delete eventFields[aDenylistedField]; + } + } + } + } + return eventFields; +} + +/** + * @deprecated + * Convenience function used by event handlers to determine if they should build and return metricsData. + * NOTE: Typically all event_handlers will check for this in addition to "recordEvent()" checking because that way + * if a client overrides "recordEvent", these checks will still take effect. + * We also test it in recordEvent() in case someone creates their own event_handler, we'd still want to exclude what needs to be excluded, in case they don't. + * @param {String} anEventType + * @param {String} (optional) topic the Figaro topic to use to look up config values + * @returns {Boolean} returns "true" if <b>either</b> "disabled()" is true or "denylistedEvents()" contains this eventType + */ +function metricsDisabledOrBlacklistedEvent(anEventType, topic) { + return metricsDisabledOrDenylistedEvent.call(this, anEventType, topic); +} + +/** + * @deprecated + * Convenience function used by event handlers to determine if they should build and return metricsData. + * NOTE: Typically all event_handlers will check for this in addition to "recordEvent()" checking because that way + * if a client overrides "recordEvent", these checks will still take effect. + * We also test it in recordEvent() in case someone creates their own event_handler, we'd still want to exclude what needs to be excluded, in case they don't. + * @param {String} anEventType + * @param {String} (optional) topic the Figaro topic to use to look up config values + * @returns {Boolean} returns "true" if <b>either</b> "disabled()" is true or "denylistedEvents()" contains this eventType + */ +function metricsDisabledOrDenylistedEvent(anEventType, topic) { + var returnValue = + disabled.call(this, topic) || + (anEventType ? denylistedEvents.call(this, topic).indexOf(anEventType) > -1 : false); + + return returnValue; +} + +/** + * @deprecated + * Config map which instructs clients to de-res (lower the resolution of) particular event fields. + * The Privacy team typically requires device capacity information to be de-resed. + * @param {String} (optional) topic the Figaro topic to use to look up config values + * @returns {Array} An array of config objects { fieldName, (optional) magnitude, (optional) significantDigits } + * Guaranteed to always return a valid array, though it may be empty if the value was unset in config + */ +function deResFields(topic) { + var returnArray = this.value('deResFields', topic); + + return returnArray || []; +} + +/** + * @deprecated + * De-res appropriate fields in the passed-in object by lowering the resolution of those field values. + * For example, a raw number of bytes "de-res"'d to MB, but without the "floor" filter, would look like these examples: + * 31708938240/1024/1024 ==> 30240 + * 15854469120/1024/1024 ==> 15120 + * 63417876480/1024/1024 ==> 60480 + * + * With the "floor" formula we replace the two least significant digits with "00" + * Doing so will convert values like so: + * + * 31708938240/1024/1024 ==> 30200 + * 15854469120/1024/1024 ==> 15100 + * 63417876480/1024/1024 ==> 60400 + * + * IMPORTANT: This action is performed in-place for performance of not having to create new objects each time. + * NOTE: Be careful not to call this method more than once for a given event, as de-resing a number more than + * once can lead to inaccurate reporting (numbers will likely be smaller than their real values) + * @param {Object} eventFields a dictionary of event data + * @param {String} (optional) topic the Figaro topic to use to look up de-res config values + * @returns {Object} the passed-in object with any fields de-resed + */ +function applyDeRes(eventFields, topic) { + if (eventFields) { + var deResFieldsConfigArray = deResFields.call(this, topic); + var fieldName; + + deResFieldsConfigArray.forEach(function (deResFieldConfig) { + fieldName = deResFieldConfig.fieldName; + if (fieldName in eventFields) { + eventFields[fieldName] = deResNumber( + eventFields[fieldName], + deResFieldConfig.magnitude, + deResFieldConfig.significantDigits + ); + } + }); + } + return eventFields; +} + +var config = /*#__PURE__*/Object.freeze({ + __proto__: null, + disabled: disabled, + blacklistedEvents: blacklistedEvents, + denylistedEvents: denylistedEvents, + blacklistedFields: blacklistedFields, + denylistedFields: denylistedFields, + removeBlacklistedFields: removeBlacklistedFields, + removeDenylistedFields: removeDenylistedFields, + metricsDisabledOrBlacklistedEvent: metricsDisabledOrBlacklistedEvent, + metricsDisabledOrDenylistedEvent: metricsDisabledOrDenylistedEvent, + deResFields: deResFields, + applyDeRes: applyDeRes +}); + +/* + * src/string.js + * mt-metricskit-utils-private + * + * Copyright © 2015-2017 Apple Inc. All rights reserved. + * + */ + +/** + **************************** PUBLIC METHODS/IVARS **************************** + */ + +/** Canned alphabets for use with convertNumberToBaseAlphabet + * Users can create their own alphabets/bases, e.g. "base61Alphabet", + * by truncating characters from the below, pre-defined, alphabets) + */ +var base10Alphabet = '0123456789'; +var base16Alphabet = base10Alphabet + 'ABCDEF'; +var base36Alphabet = base10Alphabet + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; +var base61Alphabet = base36Alphabet + 'abcdefghijklmnopqrstuvwxy'; +var base62Alphabet = base61Alphabet + 'z'; + +/** + * Test if mainString starts with subString. + * Optionally specify boolean "ignoreCase". + * @param mainString + * @param subString + * @param ignoreCase + * @returns {boolean} "false" if "mainString" or "subString" are null. + */ +function startsWith(mainString, subString, ignoreCase) { + var returnValue = false; + + if (mainString && subString) { + mainString = mainString.substr(0, subString.length); + if (ignoreCase) { + mainString = mainString.toLowerCase(); + subString = subString.toLowerCase(); + } + returnValue = mainString.indexOf(subString) === 0; + } + return returnValue; +} + +/** + * Test if one string ends with another. + * @param mainString + * @param subString + * @param ignoreCase + * @returns {boolean} "false" if "mainString" or "subString" are null. + */ +function endsWith(mainString, subString, ignoreCase) { + var returnValue = false; + if (mainString && subString) { + if (ignoreCase) { + mainString = mainString.toLowerCase(); + subString = subString.toLowerCase(); + } + // These two lines of logic (the guts) are the implementation from Prototype.js, which is well-optimized and well-tested. + var endIndex = mainString.length - subString.length; + returnValue = endIndex >= 0 && mainString.lastIndexOf(subString) === endIndex; + } + return returnValue; +} + +/** + * Removes characters in the passed-in charString from the front and back of baseString + * + * If no "charsString" is passed in (or it's non-null but identical to stringWhitespace), it tries to use the browser-platform-native "trim()" if found, otherwise trims the stringWhitespace characters (which are the same set trimmed by the built-in function): + * If a non-null "charsString" string is passed in, it will try to remove all characters within that string, regardless of their order. + * + * (NOTE: These come from WebKit's built-in "trim()" method, who's testcase lives here: + * http://code.google.com/p/v8/source/browse/branches/bleeding_edge/test/mjsunit/third_party/string-trim.js?spec=svn3842&r=3052 + * '\u0009' (HORIZONTAL TAB) + * '\u000A' (LINE FEED OR NEW LINE) + * '\u000B' (VERTICAL TAB) + * '\u000C' (FORMFEED) + * '\u000D' (CARRIAGE RETURN) + * '\u0020' (SPACE) + * '\u00A0' (NO-BREAK SPACE) + * '\u2000' (EN QUAD) + * '\u2001' (EM QUAD) + * '\u2002' (EN SPACE) + * '\u2003' (EM SPACE) + * '\u2004' (THREE-PER-EM SPACE) + * '\u2005' (FOUR-PER-EM SPACE) + * '\u2006' (SIX-PER-EM SPACE) + * '\u2007' (FIGURE SPACE) + * '\u2008' (PUNCTUATION SPACE) + * '\u2009' (THIN SPACE) + * '\u200A' (HAIR SPACE) + * '\u3000' (IDEOGRAPHIC SPACE) + * '\u2028' (LINE SEPARATOR) + * '\u2029' (PARAGRAPH SEPARATOR) + * '\u200B' (ZERO WIDTH SPACE (category Cf)'} + * NOTE: If you pass a custom "charString" and want whitespace removed as well, be sure to include the whitespace string as well + * Examples: " hello world ".trim() -> "hello world" -- " e hello world f".trim(stringWhitespace+"ef") -> "hello world" + * @param basestring is the string to trim + * @param If no "charsString" is passed in (or it's non-null but identical to stringWhitespace), it tries to use the browser-platform-native "trim()" if found, otherwise trims the stringWhitespace characters (which are the same set trimmed by the built-in function): + * If a non-null "charsString" string is passed in, it will try to remove all characters within that string, regardless of their order. + * @param forceNonNativeTrim is mostly used for testing purposes, but does what it says. + */ +function trim(baseString, charString, forceNonNativeTrim) { + var returnValue = null; + var stringWhitespace = + '\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u3000\u2028\u2029\u200B'; + var whitespaceTrimStartRegex = new RegExp('^[' + stringWhitespace + ']+'); // No need to create a new one of these objects on each call! + var whitespaceTrimEndRegex = new RegExp('[' + stringWhitespace + ']+$'); // No need to create a new one of these objects on each call! + + if (baseString) { + if (!forceNonNativeTrim && (!charString || charString == stringWhitespace) && baseString.trim) { + // Use browser-built-in trim, if it exists... + returnValue = baseString.trim(); + } else { + // NOTE: IF YOU MODIFY THIS METHOD, COPY AND TEST THE MODIFICATION TO itmsCheck.js WHICH HAS A COPY/PASTED VERSION (SANS COMMENTS) + var trimChars = null; + var startRegex = null; + var endRegex = null; + + if (charString && typeof charString !== 'undefined') { + // This is bits and pieces combined together from here: http://stackoverflow.com/questions/494035/how-do-you-pass-a-variable-to-a-regular-expression-javascript + charString = charString.replace(/([.?*+^$[\]\\(){}-])/g, '\\$1'); // If we don't do this, then if "mainString" has .'s, or other regex chars in it, the regex interprets them as part of the regex! + trimChars = '[' + charString + ']'; + startRegex = new RegExp('^' + trimChars + '+'); + endRegex = new RegExp(trimChars + '+$'); + } else { + trimChars = stringWhitespace; + startRegex = whitespaceTrimStartRegex; + endRegex = whitespaceTrimEndRegex; + } + var str = baseString.replace(startRegex, ''); + returnValue = str.replace(endRegex, ''); + } + } + return returnValue; +} + +/** + * Changes snake_case "source" string to lowerCamelCase or UpperCamelCase + * @param {String} source underscore separated sentence/list of words + * @param {Boolean} upperCamelCase - optional parameter specifying whether to capitalize the first letter, defaults to false + * @return {String} result the source parameter in lower or upper camel case + */ +function snakeCaseToCamelCase(source, upperCamelCase) { + var result = ''; + if (source) { + var words = source.toLowerCase().split('_'); + var firstChar; + + for (var i = 0; i < words.length; i++) { + firstChar = words[i][0]; + if (i !== 0 || upperCamelCase) { + firstChar = firstChar.toUpperCase(); + } + result += firstChar + words[i].slice(1); + } + } + return result; +} + +/** + * Changes snake_case "source" string to UpperCamelCase + * @param {String} source Underscore separated sentence/list of words + * @return {String} result The source parameter in upper camel case + */ +function snakeCaseToUpperCamelCase(source) { + return snakeCaseToCamelCase(source, true); +} + +/** + * Turns an object into a query param string + * @param {Object} params is the set of key-value pairs to turn into a query param string. + * @returns {String} a query param string with URI-encoded values created using the key-value pairs in the passed in object. e.g. "app=com.apple.Safari&testValue=test&eventTime=14927450" + * NOTE: The first key of the returned string is never prefaced, not with an ampersand (&) or a question mark (?) + * @example + * var paramString = _utils.string.paramString({ + * app: 'com.apple.Safari', + * testValue: 'test', + * eventTime: 14927450 + * }); + */ +function paramString(params) { + var paramString = ''; + var delimiter = ''; + var firstKey = true; + + for (var key in params) { + var value = params[key]; + if (value || value === 0 || value === false) { + paramString += delimiter + key + '=' + encodeURIComponent(value); + if (firstKey) { + delimiter = '&'; + firstKey = false; + } + } + } + return paramString; +} + +function exceptionString(className, methodName) { + return ( + 'The function ' + + className + + '.' + + methodName + + '() must be overridden with a platform-specific delegate function.' + + 'If you have no data for this function, have your delegate return null ' + + "or undefined (no 'return')" + ); +} + +/** + * Parses a user agent string for a particular product name and returns its version + * @param {String} userAgent that conforms with RFC 7231 section 5.5.3 regarding User-Agents + * @param {String} (optional) productName the name of a product identifier to search for e.g. 'iTunes'; if omitted, defaults to the first identifier + * @return {String} the version of the product, or null if none found + * @example + * versionStringFromUserAgent('iTunes/12.6 (Macintosh; OS X 10.12.4) AppleWebKit/603.1.30.0.34') returns '12.6' + * versionStringFromUserAgent('iTunes/12.6 (Macintosh; OS X 10.12.4) AppleWebKit/603.1.30.0.34', 'AppleWebKit') returns '603.1.30.0.34' + * versionStringFromUserAgent('iTunes/12.6 (Macintosh; OS X 10.12.4) AppleWebKit/603.1.30.0.34', 'Macintosh') returns null + * (strings contained in parentheses are counted as comments, not product identifiers) + */ +function versionStringFromUserAgent(userAgent, productName) { + var returnValue = null; + + productName = productName || '\\S+'; // default to the first product name + + var re = new RegExp('\\b' + productName + '/(\\S+)\\b', 'i'); + var match = re.exec(userAgent); + + if (match && match[1]) { + returnValue = match[1]; + } + + return returnValue; +} + +/** + * Takes a client ID (universally unique per device) and generates another UUID that is unique per request + * TODO: have a fallback in case clientId is unavailable + * @param {string} clientId, a base-61 UUID that uses 'z' as a delimiter + * @return {string} A generated UUID, to be used in visit stitching + */ +function requestId(clientId) { + // NOTE: The reason we integrate "clientId" into this requestId (uuid) is because there is no itms.crypto functionality in ITMLKit for creating robust UUIDs, so we + // leverage off the fact that "clientId" was created cryptographically strong in Java. + var delimiter = 'z'; + var epochTime = Date.now(); + var randomNum = Math.floor(Math.random() * 100000); + + // convert to base 36, and use uppercase since 'z' is a delimiter in clientId + epochTime = epochTime.toString(36).toUpperCase(); + randomNum = randomNum.toString(36).toUpperCase(); + + return clientId + delimiter + epochTime + delimiter + randomNum; +} + +/** + * Generates a RFC4122-compliant UUID (v4) + * See https://tools.ietf.org/html/rfc4122 + * For a discussion on the probability of collisions of version 4 UUIDs, see: + * https://en.wikipedia.org/wiki/Universally_unique_identifier#Collisions + * @param {Function} (optional) pseudoRNG a function that returns a pseudo random number between 0 and 1 + * defaults to a cryptographically strong PRNG when available or Math.random() + * which is not cryptographically strong, but can be used where a small number of collisions are acceptable + * @return {String} + * TODO: consider optimizing to use fewer calls to randomHexCharacter (i.e. switch to a randomHexString method) + */ +function uuid(pseudoRNG) { + var template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'; + var uuid = ''; + var character; + + for (var i = 0, len = template.length; i < len; i++) { + character = template.charAt(i); + + if (character === 'x') { + uuid += randomHexCharacter(pseudoRNG); + } else if (character === 'y') { + uuid += randomHexCharacter(pseudoRNG, '8', 'b'); + } else { + uuid += character; + } + } + + return uuid; +} + +/** + * Generates a random hexdecimal character between 0 and f + * @param {Function} (optional) a pseudo random number generator that returns a value between 0 and 1 + * defaults to a cryptographically strong PRNG when available or Math.random() + * which is not cryptographically strong, but can be used where a small number of collisions are acceptable + * @param {String} (optional) min the lowest character (0-f) to include, inclusive + * @param {String} (optional) max the highest character (0-f) to include, inclusive + * @return {String} + */ +function randomHexCharacter(pseudoRNG, min, max) { + var globalObject = globalScope(); + var cryptoObject = globalObject.crypto || globalObject.msCrypto; + var randomCharacter; + + if (pseudoRNG) { + randomCharacter = ((pseudoRNG() * 16) | 0).toString(16); + } else if (cryptoObject && cryptoObject.getRandomValues) { + randomCharacter = (cryptoObject.getRandomValues(new Uint8Array(1))[0] & 15).toString(16); + } else if (cryptoObject && cryptoObject.randomBytes) { + randomCharacter = cryptoObject.randomBytes(1).toString('hex')[0]; + } else { + randomCharacter = ((Math.random() * 16) | 0).toString(16); + } + + // rejection sampling: if character not in desired range, generate another one + if (min && max && (randomCharacter < min || randomCharacter > max)) { + randomCharacter = randomHexCharacter(pseudoRNG, min, max); + } + + return randomCharacter; +} + +/** + * Adapted from MTStringUtil.java, which copied from MZStringUtil.java. + * Base-2 to base-62 target alphabets are accepted. + * Alphabet order must be 0-9, then "A" stands for 10, "Z" for 35, "a" (lower-case) for 36 and "z" (lower-case) for 61. + * Not sure if there is any standard for displaying numbers higher than base-36. Lowercase letters are used to go up + * to base-62. + * + * @param {Number} number a POSITIVE base 10 number to convert + * @param {String} targetAlphabet indicates the base of the target value (by virtue of the length of the alphabet) + * as well as the characters to use during conversion. "Canned" alphabets are provided, + * but homegrown alphabets may be used by truncating values from canned alphabets. + * @return {String} a string that has been converted to targetAlphabet + */ +function convertNumberToBaseAlphabet(number, targetAlphabet) { + var returnValue = ''; + var targetRadix = targetAlphabet.length; + + if (targetRadix <= 36) { + returnValue = number.toString(targetRadix).toUpperCase(); + } else { + var remainder; + var charForRemainder; + var charArray = []; + + while (number > 0) { + remainder = number % targetRadix; + charForRemainder = targetAlphabet.charAt(remainder); + charArray.push(charForRemainder); + number = (number - remainder) / targetRadix; + } + + returnValue = charArray.reverse().join(''); + } + + if (returnValue === '') { + returnValue = '0'; + } + + return returnValue; +} + +/** + * Generates a random base62 string. If strong crypto is available, this will try to use it first. + * See https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues#Browser_compatibility + * for availability. + * @param {Boolean} hasPrefix - optional parameter specifying whether to add version/crypto indicator prefix to the string. + * @return {string} A random 24 character base62 random string if successful, or null if no strong crypto is available. + */ +function cryptoRandomBase62String(hasPrefix) { + var base62String; + // Test to make sure unsigned numbers with full 32 bits are supported + if (Math.floor(0xffffffff / 0x100) == 0xffffff) { + var globalObject = globalScope(); + var cryptoObject = globalObject.crypto || globalObject.msCrypto; + var arr; + var i; + var j; + var num; + var isCrypto; + + // UUID has 16 bytes + if (cryptoObject && cryptoObject.getRandomValues) { + arr = cryptoObject.getRandomValues(new Uint32Array(16 / Uint32Array.BYTES_PER_ELEMENT)); + isCrypto = true; + } else if (cryptoObject && cryptoObject.randomBytes) { + var b = cryptoObject.randomBytes(16); + arr = new Uint32Array(b.buffer, b.byteOffset, b.byteLength / Uint32Array.BYTES_PER_ELEMENT); + isCrypto = true; + } else { + arr = new Uint32Array(16 / Uint32Array.BYTES_PER_ELEMENT); + for (i = 0; i < arr.length; i++) { + arr[i] = Math.floor(Math.random() * Math.floor(0xffffffff)); + } + } + + if (arr) { + base62String = ''; + for (i = 0; i < arr.length; i++) { + num = arr[i]; + for (j = 0; j < 6; j++) { + // 4-byte block encoded to 6 byte base62 + base62String += base62Alphabet[num % 62]; + num = Math.floor(num / 62); + } + } + if (hasPrefix) { + // 1st char: version (currently 1) + // 2nd char: separator _ + // 3rd char: encryption type, 0 = unknown, 1 = yes, 2 = no + // 4th char: separator _ + base62String = '1_' + (isCrypto ? '1' : '2') + '_' + base62String; + } + } + } + return base62String; +} + +var string = /*#__PURE__*/Object.freeze({ + __proto__: null, + base10Alphabet: base10Alphabet, + base16Alphabet: base16Alphabet, + base36Alphabet: base36Alphabet, + base61Alphabet: base61Alphabet, + base62Alphabet: base62Alphabet, + startsWith: startsWith, + endsWith: endsWith, + trim: trim, + snakeCaseToCamelCase: snakeCaseToCamelCase, + snakeCaseToUpperCamelCase: snakeCaseToUpperCamelCase, + exceptionString: exceptionString, + paramString: paramString, + versionStringFromUserAgent: versionStringFromUserAgent, + requestId: requestId, + uuid: uuid, + randomHexCharacter: randomHexCharacter, + convertNumberToBaseAlphabet: convertNumberToBaseAlphabet, + cryptoRandomBase62String: cryptoRandomBase62String +}); + +/* + * src/cookies.js + * mt-metricskit-utils-private + * + * Copyright © 2015-2017 Apple Inc. All rights reserved. + * + */ + +/** + * + * Cookie related util methods + * @constructor + * + * Packaging note: tree-shaking does not remove unused functions from this class object + * It might be more efficient to separate the cookie delegate from the utility functions + */ +var cookies = { + /** + ************************************ PUBLIC METHODS/IVARS ************************************ + */ + + /** + * Allows replacement of one or more of this class' functions + * Any method on the passed-in object which matches a method that this class has will be called instead of the built-in class method. + * To replace *all* methods of his class, simply have your delegate implement all the methods of this class + * Your delegate can be a true object instance, an anonymous object, or a class object. + * Your delegate is free to have as many additional non-matching methods as it likes. + * It can even act as a delegate for multiple MetricsKit objects, though that is not recommended. + * + * NOTE: when the delegate function is called, it will include an additional final parameter representing the original function that it replaced. + * This allows the delegate to, essentially, call "super" before or after it does some work. + * @example: + * To override one or more methods, in place: + * eventRecorder.setDelegate({recordEvent: itms.recordEvent}); + * To override one or more methods with a separate object: + * eventRecorder.setDelegate(eventRecorderDelegate); + * (where "eventRecorderDelegate" might be defined elsewhere as, e.g.: + * var eventRecorderDelegate = {recordEvent: itms.recordEvent, + * sendMethod: 'itms'}; + * To override one or more methods with an instantiated object from a class definition: + * eventRecorder.setDelegate(new EventRecorderDelegate()); + * (where "EventRecorderDelegate" might be defined elsewhere as, e.g.: + * function EventRecorderDelegate() { + * } + * EventRecorderDelegate.prototype.recordEvent = itms.recordEvent; + * EventRecorderDelegate.prototype.sendMethod = function sendMethod() { + * return 'itms'; + * }; + * To override one or more methods with a class object (with "static" methods): + * eventRecorder.setDelegate(EventRecorderDelegate); + * (where "EventRecorderDelegate" might be defined elsewhere as, e.g.: + * function EventRecorderDelegate() { + * } + * EventRecorderDelegate.recordEvent = itms.recordEvent; + * EventRecorderDelegate.sendMethod = function sendMethod() { + * return 'itms'; + * }; + * @param {Object} Object or Class with delegate method(s) to be called instead of default (built-in) methods. + * @returns {Boolean} true if one or more methods on the delegate object match one or more methods on the default object, + * otherwise returns false. + */ + setDelegate: function setDelegate(delegate) { + return attachDelegate(this, delegate); + }, + + /** + * The cookie string, e.g. "iTunes.cookie" (iTunes desktop), "iTunes.cookieForDefaultURL" (HTML iOS), "itms.cookie" (itml app), "document.cookie" (browser) + * NOTE: Callers should override this method if they want to supply a different cookie. + * @overridable + */ + cookie: function cookie() { + var cookieOwnerObject; + + if (typeof window !== 'undefined' && 'iTunes' in window && 'cookie' in iTunes) { + cookieOwnerObject = iTunes; + } else if (typeof itms !== 'undefined' && isDefined(itms.cookie)) { + cookieOwnerObject = itms; + } else if (typeof document !== 'undefined') { + cookieOwnerObject = document; + } else { + throw 'cookies.cookie: No cookie object available'; + } + + return cookieOwnerObject.cookie; + }, + + // NOTE: MetricsKit does not currently need to set cookies, so setting functions are commented out until they are needed + /** + * Normal/Primary cookie setter method. + * Invokes JavaScript's "escape()" function on "cookieValue" before passing to "cookies.setUnescaped()" + * + * Set cookie with the given name and value at the path "/" + * @param cookieName the name of the cookie; must not contain whitespace or semicolons. + * @param cookieValue must not contain semicolons or whitespace. Use escape() if necessary + * @param lifespanInSeconds may be one of: 1. the time-to-live in seconds, 2. null to expire at browser (session) termination, 3. negative to delete a cookie + * @param path the path to use (optional,if null, defaults to "/") + * @param domain the domain to use (if null, defaults to the current domain) + */ + // this.set = function set(cookieName, cookieValue, lifespanInSeconds, path, domain) { }; + + /** + * "Normal Use" cookie getter method. + * Invokes JavaScript's "unescape()" function on value returned from "cookies.getUnescaped()" and returns that unescaped value. + */ + get: function get(cookieName) { + // NOTE: IF YOU MODIFY THIS METHOD, COPY AND TEST THE MODIFICATION TO itmsCheck.js WHICH HAS A COPY/PASTED VERSION (SANS COMMENTS) + var returnValue = this.getUnescaped(cookieName); + if (returnValue) returnValue = unescape(returnValue); + return returnValue; + }, + + // NOTE, The jingle "setUnescaped" method has a lot of special-case code both for devices and ITML. + // That is important functionality for setting cookies on those platforms but we don't currently need to set cookies (we used to do it as a device workaround in iOS6) and + // to include it here, we would need to create platform-specific delegates, so let's save all that mess for the day we actually need this functionality at + // which point you SHOULD grab and adapt the code from Jingle: + setUnescaped: function setUnescaped(cookieName, cookieValue, lifespanInSeconds, path, domain) {}, + + /** + * Funnel-point cookie-getting method. + * Parsing document.cookie is simple, but there are a lot of quirks. + * + * The simple format is "a=b; c=d;", but according to RFC 2965 (from 2006, but only Opera supports it), + * any amount of whitespace (including none) is optional as a separator. + * NOTE:KBERN: We trim whitespace from the beginning and end of both keys and values based on my reading of: + * http://tools.ietf.org/html/rfc2965 + * on page 3 it says, + * "NOTE: The syntax above allows whitespace between the attribute and the = sign.", and so even though + * Meaning that a) there does have to be an "=" sign, and b) there can be whitespace on both sides of it. + * Safari seems to collapse whitespace around the "=", but there is no guarantee that all browsers will + * behave that way on all platforms, therefore I think that both the keys and values need to be trimmed of + * both leading and trailing spaces. + * + * According to the most widely supported spec (the 1995 Netscape cookie draft doc), the name-value pairs are + * "a sequence of characters excluding semi-colon, comma and white space". This means the value can contain + * the equals sign, and any other number of weird characters! + * + * In short, don't mess with this function. + * + * <rdar://problem/8123192> Javascript cookie parsing is chopping off trailing = signs + */ + getUnescaped: function getUnescaped(cookieName) { + var result = null; + + // NOTE: IF YOU MODIFY THIS METHOD, COPY AND TEST THE MODIFICATION TO itmsCheck.js WHICH HAS A COPY/PASTED VERSION (SANS COMMENTS) + var cookieString = this._getRaw(); + if (cookieString && cookieName) { + var splitCookies = cookieString.split(';'); + + // GO THROUGH THE COOKIES BACKWARDS BECAUSE... + // (This comment, and searching from back->front is from Dojo: http://www.bedework.org/trac/bedework/browser/trunk/deployment/resources/javascript/dojo-0.4.1-ajax/src/io/cookie.js?rev=1164 + // I haven't tried to reproduce this, but it's no skin off our backs to go backwards anyway, so...) + // Which cookie should we return? + // If there are cookies set for different sub domains in the current + // scope there could be more than one cookie with the same name. + // I think taking the last one in the list takes the one from the + // deepest subdomain, which is what we're doing here. + for (var i = splitCookies.length - 1; !result && i >= 0; i--) { + var aCookie = splitCookies[i]; + var separatorIndex = aCookie.indexOf('='); + + if (separatorIndex > 0) { + if (separatorIndex + 1 == aCookie.length) { + result = ''; // there *is* a cookie key, but there is nothing to the right of the "=" + } else { + // Trim all leading and trailing whitespace from key... + var cookieKey = trim(aCookie.substring(0, separatorIndex)); + + if (cookieKey == cookieName) { + // Trim all leading and trailing whitespace from the value as well, since there may be whitespace to the right of the "=" sign + result = trim(aCookie.substring(separatorIndex + 1)); + } + } + } + } + } + return result; + }, + + /** + * adding a cover accessor to document.cookie so that in iOS 4.2 and newer we can use iTunes.cookies instead (allows access to private storage) + */ + + /** + * Clear a cookie (expire/delete/remove it) + * @param {Object} cookieName + */ + remove: function remove(cookieName, domain) { + return this.setUnescaped(cookieName, '.', this.EXPIRE_NOW, null, domain); + }, + + // PRIVATE METHODS: + + // NOTE: MetricsKit does not currently need to set cookies, so setting functions are commented out until they are needed + /** + * @param val the raw Cookie string (Webkit), or a Cookie dict (ITML) to be set. + */ + // this._setRaw = function _setRaw(val) { }; + + /** + * @returns all Cookies as a string. + */ + _getRaw: function _getRaw() { + return this.cookie() || ''; + } +}; + +// CONSTANTS: +// Convenient lifespanInSeconds values: +cookies.EXPIRE_NOW = -1; +cookies.EXPIRE_SESSION = null; // or "0" +cookies.EXPIRE_ONE_SECOND = 1; +cookies.EXPIRE_ONE_MINUTE = cookies.EXPIRE_ONE_SECOND * 60; +cookies.EXPIRE_ONE_HOUR = cookies.EXPIRE_ONE_MINUTE * 60; +cookies.EXPIRE_ONE_DAY = cookies.EXPIRE_ONE_HOUR * 24; +cookies.EXPIRE_ONE_WEEK = cookies.EXPIRE_ONE_DAY * 7; +cookies.EXPIRE_ONE_MONTH = cookies.EXPIRE_ONE_DAY * 31; +cookies.EXPIRE_ONE_YEAR = cookies.EXPIRE_ONE_DAY * 365; +cookies.EXPIRE_ONE_SIDEREAL_YEAR = cookies.EXPIRE_ONE_DAY * 365.25; // (31556926279 or so)... For those who want decades long accuracy :-( ... of course we could also make special day times since a day is really 24 hours and 2 milliseconds long :-) +cookies.EXPIRE_SIX_MONTHS = cookies.EXPIRE_ONE_DAY * 180; // <rdar://problem/11067278> Cookies: reduce max age to 6 months + +/* + * src/utils/delegates_info.js + * mt-metricskit-utils-private + * + * Copyright © 2016 Apple Inc. All rights reserved. + * + */ + +/** + * Used to store information + * about attached delegates as a tree, + * in which delegates may contain their own + * list of "child" delegates as an array + * @type {Object} + * @example + * // If metricsKit.attachDelegate(delegatesITML) + * delegatesMap = { + '@amp/mt-metricskit5.1.0': { + name: '@amp/mt-metricskit', + version: '5.1.0' + // The accessor delegatesMap[...] would not run correctly if initialized + // at the same time as this object, but since it's added after delegatesMap + // is created, it works + delegates: [delegatesMap['@amp/mt-metricskit-delegates-itml3.1.2']] + // The above array contains a pointer to another object in delegatesMap + } + '@amp/mt-metricskit-delegates-itml3.1.2': { + name: '@amp/mt-metricskit-delegates-itml', + version: '3.1.2' + } + } + */ +var delegatesMap = {}; + +/* + * Used to keep a record of which delegates have been added to which targets + * for use with deduping the child 'delegates' field of a delegate object + * (The reason for using this object instead of adding it to the delegatesMap + * or changing the delegates subfield in delegatesMap to be an object + * is that delegatesMap represents the value of the mt-metricskit base field + * xpDelegatesInfo, which requires a certain format. + * This map is a theoretical extension of delegatesMap, to facilitate constant + * time lookup for use with deduping.) + * @type {Object} + * @example + * After mt-metricskit adds delegates-itml and delegates-html as delegates + * and delegates-itml has added base-events as a delegate + * { + * 'mt-metricskit2.1.1': ['mt-metricskit-delegates-itml3.1.5', 'mt-metricskit-delegates-html0.1.3'], + * 'mt-metrickit-delegates-itml3.1.5': ['mt-metricskit-base-events1.1.2'] + * } + */ +var dedupingMap = {}; + +/** + * @param {Object} delegate The object to retrieve name & version info off of + * @param {function} delegate.mtName Returns the name of the delegate as a string + * @param {function} delegate.mtVersion Returns the version of the delegate as a string + * @returns {Object} Contains the passed-in delegate's info, esp. name & version + */ +var createDelegateInfoObject = function createDelegateInfoObject(delegate) { + var delegateInfo = {}; + + if (typeof delegate.mtName === 'function' && typeof delegate.mtVersion === 'function') { + // Add delegate name, version, and any previously-attached "child" delegates to delegatesInfoList + delegateInfo.name = delegate.mtName(); + delegateInfo.version = delegate.mtVersion(); + } + + return delegateInfo; +}; + +/** + * Returns a concatted string of the passed-in delegate's + * name and version, to be used when storing the delegate in delegatesMap + * @param {Object} delegate Delegate to create key string from + * @param {function} delegate.mtName Returns the name of the delegate as a string + * @param {function} delegate.mtVersion Returns the version of the delegate as a string + * @returns {String} The concatted name and version of the passed-in delegate + * @example + * "mt-metricskit-delegates-itml3.1.2" + */ +var createDelegateKey = function createDelegateKey(delegate) { + var delegateKey; + if (typeof delegate.mtName === 'function' && typeof delegate.mtVersion === 'function') { + delegateKey = delegate.mtName() + delegate.mtVersion(); + } + return delegateKey; +}; + +/** + * Creates delegate info objects for passed-in target object and delegate object, + * and stores them in the delegatesMap. The info object of the delegate being attached + * to the target will be stored in the target's info object's 'delegates' field + * @param {Object} target The object being partially or wholly overwritten by the delegate + * @param {Object} delegate The object overwriting the target object's functionality + * @param {function} target.mtName Returns the name of the target as a string + * @param {function} target.mtVersion Returns the version of the target as a string + * @param {function} delegate.mtName Returns the name of the delegate as a string + * @param {function} delegate.mtVersion Returns the version of the delegate as a string + */ +function storeDelegateInfo(target, delegate) { + var targetKey = createDelegateKey(target); + var delegateKey = createDelegateKey(delegate); + if (targetKey && delegateKey) { + // Create delegate info objects (containing delegate's name & version) + // and add to delegatesMap + if (!delegatesMap[delegateKey]) { + delegatesMap[delegateKey] = createDelegateInfoObject(delegate); + } + if (!delegatesMap[targetKey]) { + delegatesMap[targetKey] = createDelegateInfoObject(target); + dedupingMap[targetKey] = {}; + } + // Add delegate's info object to target's delegates array in delegatesMap + if (delegatesMap[targetKey].delegates) { + if (!dedupingMap[targetKey][delegateKey]) { + delegatesMap[targetKey].delegates.push(delegatesMap[delegateKey]); + } + } else { + delegatesMap[targetKey].delegates = [delegatesMap[delegateKey]]; + } + dedupingMap[targetKey][delegateKey] = true; + } +} + +/** + * Return the delegate object stored in delegatesMap + * for the passed-in delegate + * @returns {Object} The stored delegate object for the passed-in delegate + */ +function getStoredDelegateObject(delegate) { + return delegatesMap[createDelegateKey(delegate)]; +} + +var delegates_info = /*#__PURE__*/Object.freeze({ + __proto__: null, + storeDelegateInfo: storeDelegateInfo, + getStoredDelegateObject: getStoredDelegateObject +}); + +/* + * src/key_value.js + * mt-metricskit-utils-private + * + * Copyright © 2015-2017 Apple Inc. All rights reserved. + * + */ + +/** + * Takes a SINGLE searchSource and string representation of an object path (a namespace) and returns the object at that subpath (possibly after first creating it) + * If the objects in the path do not already exist off of searchSource and "createIfNeeded" is true, this method will create all the JavaScript objects required to make it a valid object path + * If any component already exists, it will be maintained. New components are created as "{}" + * e.g. after this call: + * keyValue.valueForKeyPath(rootObject, 'foo.bar.Tot') + * there will always be a valid JavaScript object of: + * rootObject.foo.bar.Tot + * @param searchSource a key/value object + * @param keyPath a simple property key name, e.g. "foo", or a nested property key name "path", e.g. foo.bar.tot + * @return the discovered and/or created object at the specified path extending off the specified searchSources + * If "createIfNeeded" is not specified, and keyPath or searchSources are not both valid, "undefined" will be returned (since "null" could be a valid return value stored at some keyPath) + * If the goal of the caller is simply to create the object path and "createIfNeeded" has been specified, the return value may be ignored. + * @example valueForKeyPath("foo", {"bar":10, "foo":12}); returns "12" + */ +function _valueForKeyPath(keyPath, searchSource, createIfNeeded) { + var tailObject = searchSource; + + if (keyPath && searchSource) { + var objectStrings = keyPath.split('.'); + + for (var ii = 0; tailObject && ii < objectStrings.length; ii++) { + var anObjectString = objectStrings[ii]; + if (!(anObjectString in tailObject) && createIfNeeded) { + tailObject[anObjectString] = {}; + } + if (anObjectString in tailObject) { + tailObject = tailObject[anObjectString]; + } else { + tailObject = null; + } + } + } + return tailObject; +} + +/** + ************************************ PUBLIC METHODS/IVARS ************************************ + */ + +/** + * Takes one or more searchSources and string representation of an object path (a namespace) and returns the object at that subpath + * @param {String} keyPath a simple property key name, e.g. "foo", or a nested property key name "path", e.g. foo.bar.tot + * @param {varargs} searchSources at least one key/value object(s), or array(s) of key/value objects, or list(s) of key/value objects. + * Later sets of key/value pairs overwrite earlier sets. + * Callers are not asked to pass a unified (coalesced) set of key/value objects because that is a much more expensive operation + * to preform each time we are searching for a keyPath. + * @return the discovered object at the specified path extending off the specified searchSources. Later sets of key/value pairs overwrite earlier sets. + * @example valueForKeyPath("foo", {"bar":10, "foo":12}); returns "12" + * @example valueForKeyPath("foo.cat", [{"bar":10, "foo":12}, {"bar":10, "foo":{"cat":"meow", "dog":"ruff"}}]); returns "meow" + * @example valueForKeyPath("foo.cat", {"bar":10, "foo":12}, {"bar":10, "foo":{"cat":"meow", "dog":"ruff"}}); returns "meow" + */ +function valueForKeyPath(keyPath /*, searchSources<varargs>*/) { + var returnValue = null; + + if (keyPath && arguments.length > 1) { + // Pass in all sources after the first ("keyPath"). We do this even if there's only one param, in case that one param is an object instead of an array + var normalizedSearchSources = sourcesArray(Array.prototype.slice.call(arguments, 1)); + + // Now we just loop through our normalizedSearchSources looking for "keyPath" in any of them. + // We start at the end and look backwards, because the later sources take precedence over earlier ones. + for (var ii = normalizedSearchSources.length - 1; ii >= 0; ii--) { + var aSearchSource = normalizedSearchSources[ii]; + + returnValue = _valueForKeyPath(keyPath, aSearchSource); + if (isDefinedNonNull(returnValue)) { + break; + } + } + } + return returnValue; +} + +/** + * If the objects in the path do not already exist off of searchSource, this method will create all the JavaScript objects required to make it a valid object path + * If any component already exists, it will be maintained. New components are created as "{}" + * e.g. after this call: + * keyValue.createObjectAtKeyPath(rootObject, 'foo.bar.Tot') + * there will always be a valid JavaScript object of: + * rootObject.foo.bar.Tot + * @param searchSource a key/value object + * @param keyPath a simple property key name, e.g. "foo", or a nested property key name "path", e.g. foo.bar.tot + * @return the created object at the specified path extending off the specified searchSources + * If the goal of the caller is simply to create the object path and "createIfNeeded" has been specified, the return value may be ignored. + */ +function createObjectAtKeyPath(keyPath, searchSource) { + return _valueForKeyPath(keyPath, searchSource, true); +} + +/** + * Expands the sources param, and any varargs that might follow it and puts them all into an array. + * Items within "sources" or varargs can, themselves, be arrays, in which case they will be decomposed and their items added to the top level of the returned array. + * @example sourcesArray("foo", {"bar":10, "foo":12}); returns ["foo", {"bar":10, "foo":12}] + * @example sourcesArray("foo.cat", [{"bar":10, "foo":12}, {"bar":10, "foo":{"cat":"meow", "dog":"ruff"}}]); returns ["foo.cat", {"bar":10, "foo":12}, {"bar":10, "foo":{"cat":"meow", "dog":"ruff"}}] + * @example sourcesArray("foo.cat", {"bar":10, "foo":12}, {"bar":10, "foo":{"cat":"meow", "dog":"ruff"}}); returns ["foo.cat", {"bar":10, "foo":12}, {"bar":10, "foo":{"cat":"meow", "dog":"ruff"}}] + * @param sources an object, an array of objects, an array of objects where some objects are themselves arrays + * @param varargs additional objects to be added to the returned array. + * @returns {Array} + */ +function sourcesArray(sources /*, varargs*/) { + var returnValue = []; + var arrayifiedSources = []; + + // This will add in the individual searchSources whether they are in a single object or an array... + arrayifiedSources = arrayifiedSources.concat(sources); + // This will add in anything that "arguments" had as varargs... + if (arguments && arguments.length > 1) { + arrayifiedSources = arrayifiedSources.concat(Array.prototype.slice.call(arguments, 1)); + } + + // If any of the items in "sources" is already an array, this loop will expand it and add each element as an individual source. + // We only do this one level deep (i.e. we don't look to see if there are arrays within arrays in this list). + for (var ii = 0; ii < arrayifiedSources.length; ii++) { + var arrayItem = arrayifiedSources[ii]; + returnValue = returnValue.concat(arrayItem); + } + + return returnValue; +} + +var key_value = /*#__PURE__*/Object.freeze({ + __proto__: null, + valueForKeyPath: valueForKeyPath, + createObjectAtKeyPath: createObjectAtKeyPath, + sourcesArray: sourcesArray +}); + +/* + * src/metrics/utils/event_fields.js + * mt-metricskit-utils-private + * + * Copyright © 2015-2017 Apple Inc. All rights reserved. + * + */ + +/** + ************************************ PUBLIC METHODS/IVARS ************************************ + */ + +/** + * Takes one or more eventFields objects, cleans them (removes keys that are typeof 'function', keys with 'null' values, keys with 'undefined' values), + * merges them (later objects take precedence), and returns a single object with the union of all remaining fields. + * Passed in objects are treated as immutable and so will never be modified. + * @param eventFields an object with keys and values OR an array of objects with keys and values. + * If only one parameter is provided, the return value will be the non-null values of that single object. + * @param varargs additional objects to be merged in, similar to eventFields (i.e. dictionaries, arrays of dictionaries, or some combination of the two). + * Later objects take precedence over earlier ones. + * @return a new object with the union of all non-function, non-null and non-undefined fields. + * Passed in objects are treated as immutable and so will never be modified. + * @example mergeAndCleanEventFields({}) ===> {} + * @example mergeAndCleanEventFields(null) ===> {} + * @example mergeAndCleanEventFields({"foo":10}) ===> {"foo":10} + * @example mergeAndCleanEventFields({"foo":10, "bar":null}) ===> {"foo":10} + * @example mergeAndCleanEventFields({"foo":10, "bar":null}, {"cat":null}) ===> {"foo":10} + * @example mergeAndCleanEventFields({"foo":10, "bar":null}, {"cat":null, "mouse":"gray"}) ===> {"foo":10, "mouse":"gray"} + * @example mergeAndCleanEventFields({"foo":10, "bar":null}, {"cat":null, "mouse":"gray", "dog":"bark"}) ===> {"foo":10, "mouse":"gray", "dog":"bark"} + * @example mergeAndCleanEventFields({"foo":10, "bar":null}, {"cat":null, "mouse":"gray", "dog":"bark", "foo":11}) ===> {"foo":11, "mouse":"gray", "dog":"bark"} + * @example mergeAndCleanEventFields({"foo":10, "bar":null}, {"cat":null, "mouse":"gray", "dog":"bark", "foo":11}, {"foo":12}) ===> {"foo":12, "mouse":"gray", "dog":"bark"} + */ +function mergeAndCleanEventFields(eventFields /*, varargs*/) { + var argumentsArray = [false, false, false].concat(Array.prototype.slice.call(arguments)); + // expand argumentsArray, in case it contains arrays) + var expandedArgumentsArray = []; + + for (var ii = 0; ii < argumentsArray.length; ii++) { + var itemToPush = argumentsArray[ii]; + // Either push each item in this item... + if (itemToPush && itemToPush.constructor === Array) { + for (var jj = 0; jj < itemToPush.length; jj++) { + expandedArgumentsArray.push(itemToPush[jj]); + } + // or push the item itself (if it is not an array) + } else { + expandedArgumentsArray.push(itemToPush); + } + } + return copyKeysAndValues.apply(null, expandedArgumentsArray); +} + +/** + * This method is the workhorse of all the various eventHandlers. + * It will take all of the parameters of the callers "metricsData()" method, merge them together, + * invoke accessors on their known fields, and return the resultant map. + * @param eventHandler the calling eventHandler + * @param knownFields the calling eventHandler's list (array) of strings that are that handler's known field values. + * If the caller has accessors to be invoked, they must be present in the "knownFields" array + * @param {Boolean} includeAllKnownFields if false, only known field accessors that match caller provided field names will be invoked. + * @returns {Arguments} all of the arguments that the calling eventHandler received. + * @example: + * Page.prototype.metricsData = function(pageId, pageType, pageContext, eventFieldsMapN(varargs)) { + * var pageFields = { pageId: pageId, pageType: pageType, pageContext: pageContext }; + * return utils.eventFields.processMetricsData(this, this.knownFields(), true, pageFields, eventFieldsMapN); }); + */ +function processMetricsData(eventHandler, knownFields, includeAllKnownFields, callerSuppliedEventFieldsMapsArray) { + var callerProvidedFields = mergeAndCleanEventFields(callerSuppliedEventFieldsMapsArray); + // Initialize returnValue with the passed-in fields in case there are fields we haven't contemplated or don't have accessor methods for, they will still be included. + var returnValue = callerProvidedFields; + + if (eventHandler && knownFields) { + var knownFieldValues = {}; + + if (!includeAllKnownFields) { + // only include known field names that were also included in caller provided maps + knownFields = knownFields.filter(function (fieldName) { + return fieldName in callerProvidedFields; + }); + } + if (knownFields.length) { + for (var ii = 0; ii < knownFields.length; ii++) { + var knownFieldName = knownFields[ii]; + var knownFieldAccessor = eventHandler[knownFieldName]; + + if (isFunction(knownFieldAccessor)) { + // NOTE: If the accessor method prefers to use a value from the passed-in callerProvidedFields, it must do that on its own. + knownFieldValues[knownFieldName] = knownFieldAccessor.call(eventHandler, callerProvidedFields); + } + } + } + + returnValue = mergeAndCleanEventFields(returnValue, knownFieldValues); + } + + return returnValue; +} + +/** + * Returns an object containing the intersection of properties in + * data and matching string values in the fieldMap property corresponding to 'sectionName' + * ( e.g. fieldMap.custom[sectionName] is an object containing arrays of strings which + * correspond to the keys desired in the mappedFields object ) + * @param {Object} data The model data corresponding to element we're mapping fields for + * @param {String} sectionName Specifies which section of the fieldMap to use (eg: 'impressions', 'location', or 'custom') + * @param {Object} fieldsMap contains one or more field mapping(s) + * @param {Function} (optional) onError callback to be invoked with an error message; e.g. console.error + * @return {Object} Contains intersection of data and fieldsMap values + * @example + * // where impressionFieldsMapSection = { + * // impressionType: ['type', 'impressionType'], + * // id: ['targetId', 'id'] + * //}; + * applyFieldsMap({type: 'button', id: '123', name: 'playbutton'}, 'impressions') + * // returns {impressionType: 'button', id: '123'} + */ +function applyFieldsMap(data, sectionName, fieldsMap, onError) { + var fieldsMapSection; + var mappedFields; + var errorMessage; + + if (data && sectionName && fieldsMap) { + mappedFields = {}; + fieldsMapSection = valueForKeyPath(sectionName, fieldsMap, fieldsMap.custom); + if (fieldsMapSection) { + var i; + var value; + if (isArray(fieldsMapSection)) { + for (i = 0; i < fieldsMapSection.length; ++i) { + value = data[fieldsMapSection[i]]; + if (isDefinedNonNull(value)) { + mappedFields[fieldsMapSection[i]] = value; + } + } + } else if (isObject(fieldsMapSection)) { + for (var key in fieldsMapSection) { + for (i = 0; i < fieldsMapSection[key].length; ++i) { + value = valueForKeyPath(fieldsMapSection[key][i], data); + if (isDefinedNonNull(value)) { + mappedFields[key] = value; + break; + } + } + } + } else { + errorMessage = + 'metrics: incorrect data type provided to applyFieldsMap (only accepts objects and Arrays)'; + } + } else { + errorMessage = 'metrics: unable to get ' + sectionName + ' section from fieldsMap'; + } + } else { + var missingArgs = []; + + if (!data) { + missingArgs.push('data'); + } + if (!sectionName) { + missingArgs.push('sectionName'); + } + if (!fieldsMap) { + missingArgs.push('fieldsMap'); + } + + errorMessage = 'metrics: missing argument(s): ' + missingArgs.join(',') + ' not provided to applyFieldsMap'; + } + + if (errorMessage && isFunction(onError)) { + onError(errorMessage); + } + + return mappedFields; +} + +var event_fields = /*#__PURE__*/Object.freeze({ + __proto__: null, + mergeAndCleanEventFields: mergeAndCleanEventFields, + processMetricsData: processMetricsData, + applyFieldsMap: applyFieldsMap +}); + +/* + * src/metrics/utils/network.js + * mt-metricskit + * + * Copyright © 2015-2017 Apple Inc. All rights reserved. + * + */ + +/** + * Makes an XHR GET request + * @param {String} url of the request + * @param {Function} onSuccessHandler function to execute on request success (response returned) + * @param {Function} onFailureHandler function to execute on request failure (error returned) + */ +function makeAjaxGetRequest(url, onSuccessHandler, onFailureHandler) { + makeAjaxRequest(url, 'GET', null, onSuccessHandler, onFailureHandler); +} + +/** + * Creates, modifies, and sends an XMLHttpRequest object + * @param {String} url url of endpoint + * @param {String} method "GET", "POST", etc. + * @param {*} data data to send to endpoint if method is "POST", "PUT", etc. + * @param {Function} [onSuccess] optional function to execute on request success (takes response returned) + * @param {Function} [onFailure] optional function to execute on request failure (takes error returned and optional status code) + * @param {Object} [options] optional dictionary of options which define how to modify the XMLHttpRequest + * @param {boolean} [options.async] optional boolean which determines if the request should be asynchronous + * @param {Number} [options.timeout] optional which determines request timeout + * @param {Number} [options.withCredentials] optional which determines request withCredentials + */ +function makeAjaxRequest(url, method, data, onSuccess, onFailure, options) { + var request = new XMLHttpRequest(); + data = data || undefined; + options = options || {}; + onSuccess = isFunction(onSuccess) ? onSuccess : function () {}; + onFailure = isFunction(onFailure) ? onFailure : function () {}; + // Sets async to true by default + var async = options.async === false ? false : true; + + // synchronous requests should not use the timeout property: + // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/timeout + if (options.timeout && async) { + request.timeout = options.timeout; + } + + request.onload = function onload() { + // Successful response status is defined as 2xx by default + if (request.status >= 200 && request.status < 300) { + onSuccess(request.response); + } else { + // Pass in optional status code so status logic can be performed on failure + onFailure( + new Error('XHR error: server responded with status ' + request.status + ' ' + request.statusText), + request.status + ); + } + }; + request.onerror = function onError() { + onFailure(new Error('XHR error')); + }; + + request.open(method, url, async); + + // Because of rdar://72864343, we allow callers to optionally specify "withCredentials". + // The default value is true, which allows CORS requests to have cookies set on response + // (e.g. we're itunes.apple.com, userxp is xp.apple.com). + request.withCredentials = typeof options.withCredentials === 'boolean' ? options.withCredentials : true; + request.setRequestHeader('Content-type', 'application/json'); + + request.send(data); +} + +var network = /*#__PURE__*/Object.freeze({ + __proto__: null, + makeAjaxGetRequest: makeAjaxGetRequest, + makeAjaxRequest: makeAjaxRequest +}); + +/* + * src/metrics/utils/sampling.js + * mt-metricskit-utils-private + * + * Copyright © 2015-2017 Apple Inc. All rights reserved. + * + */ + +/** + ************************************ PRIVATE METHODS/IVARS ************************************ + */ +var _sessions = {}; + +/** + * Manually clears an active sampling session + * @param {String} sessionName + */ +var _clearSession = function _clearSession(sessionName) { + if (_sessions[sessionName]) { + clearTimeout(_sessions[sessionName]); + _sessions[sessionName] = null; + } +}; + +/** + ****************************** PSEUDO-PRIVATE METHODS/IVARS ********************************** + * These functions need to be accessible for ease of testing, but should not be used by clients + */ +function _utClearSessions() { + for (var sessionName in _sessions) { + _clearSession(sessionName); + } +} + +/** + ************************************ PUBLIC METHODS/IVARS ************************************ + */ + +/** + * A random lottery that is successful with samplingPercentage frequency + * @param {Number} samplingPercentage (between 0 and 1) + * @return {Boolean} whether or not the lottery was successful + */ +function lottery(samplingPercentage) { + return Math.random() < samplingPercentage; +} + +/** + * Determines whether a particular sampling session is active + * @param {String} sessionName the name associated with a particular session + * @param {Number} sessionSamplingPercentage (between 0 and 1) + * @param {Number} sessionDuration (in ms) + * @return {Boolean} whether or not the sampling session associated with sessionName is currently active + */ +function sessionSampled(sessionName, sessionSamplingPercentage, sessionDuration) { + var returnValue; + + // if a timer is currently running, we are sampled in to this session + if (_sessions[sessionName]) { + returnValue = true; + } else { + // roll the dice + var sampleNow = lottery(sessionSamplingPercentage); + + // check if we need to enable sampling for sessionDuration ms + if (sampleNow && sessionDuration > 0) { + _sessions[sessionName] = setTimeout(_clearSession.bind(null, sessionName), sessionDuration); + } + + returnValue = sampleNow; + } + + return returnValue; +} + +/** + * Determines whether an eventType should be sampled. + * Session sampling (sessionSamplingPercentage and sessionDuration) will be checked first, and samplingPercentage will be used as a fallback. + * @param {Boolean} (optional) samplingForced whether to always sample in. Default false. + * @param {Number} (optional) sessionSamplingPercentage (between 0 and 1) the frequency at which to initiate sampling sessions for eventType. Default 0. + * @param {Number} (optional) sessionDuration the duration, in milliseconds, of sampling sessions for this eventType. Default 0. + * @param {Number} (optional) samplingPercentage (between 0 and 1) the frequency at which to sample in individual events. Default 0. + * @example sampling.isSampledIn('pageRender', null, 0.05, 60000); + * @return {Boolean} whether or not eventType is currently sampled in + */ +function isSampledIn(eventType, samplingForced, sessionSamplingPercentage, sessionDuration, samplingPercentage) { + return ( + samplingForced || + sessionSampled(eventType, sessionSamplingPercentage, sessionDuration) || + lottery(samplingPercentage) + ); +} + +var sampling = /*#__PURE__*/Object.freeze({ + __proto__: null, + _utClearSessions: _utClearSessions, + lottery: lottery, + sessionSampled: sessionSampled, + isSampledIn: isSampledIn +}); + +/* + * src/storage.js + * mt-metricskit-utils-private + * + * Copyright © 2015-2017 Apple Inc. All rights reserved. + * + */ + +/** + ************************************ PRIVATE METHODS/IVARS ************************************ + */ + +var CONSTANTS = { + STORAGE_TYPE: { + LOCAL_STORAGE: 'localStorage', + SESSION_STORAGE: 'sessionStorage' + } +}; + +/** + * Cover function for sessionStorage/localStorage. + * Some clients do not have implementations of storage objects, and some are platform-specific (e.g. iTunes.sessionStorage) + * This helper method will shim storage object functionality as necessary + * @param {object} storageObject - this is either the platform-specific implementation of session/localStorage, or null if none exists + * @returns {object} a storage object, either the provided platform-specific one or a placeholder/shimmed version + */ +var _storageObject = function _storageObject(storageObject) { + var aStorageObject = null; // We'll capture this variable as a singleton in the closure below... + var errorShown = false; + + return function () { + if (!storageObject) { + if (!errorShown) { + console.error( + 'storageObject: storage object not found. Override this function if there is a platform-specific implementation' + ); + errorShown = true; // only show error the first time + } + // Let's not stop the whole app by "throw"ing here, and let's not make all callers be required to check for undefined return values. + // We'll just create a placeholder sessionStorage object which will not hold values across page JS contexts, but at least it will hold them for *some* time... + if (!aStorageObject) { + aStorageObject = { + storage: {}, + getItem: function (key) { + return this.storage[key]; + }, + + setItem: function (key, value) { + this.storage[key] = value; + }, + + removeItem: function (key) { + delete this.storage[key]; + } + }; + } + } else { + aStorageObject = storageObject; + } + return aStorageObject; + }; +}; + +/** + * Fetches the given storage object from the global object. This function wraps around a try-catch to avoid any exception from being thrown + * in cases where the local storage API is disabled (say for example by disabling cookies in Safari preferences) + * @param {Object} storageObjectType the storage object type to be evaluated on the window object. Possible values are localStorage or sessionStorage and are defined in CONSTANTS.STORAGE_TYPE object above. + * @return {Object} the storage object or null if key doesn't exist in storage or the storage api is disabled + */ +function _defaultStorageObject(storageObjectType) { + var storageObject = null; + var storageObjectClass = null; + var isLocalStorageObjectType = storageObjectType === CONSTANTS.STORAGE_TYPE.LOCAL_STORAGE; + try { + storageObjectClass = isLocalStorageObjectType ? typeof localStorage : typeof sessionStorage; + if (storageObjectClass !== 'undefined') { + storageObject = isLocalStorageObjectType ? localStorage : sessionStorage; + } else { + storageObject = null; + } + } catch (e) { + // We allow the current process to run without interruption instead of bringing the app down + storageObject = null; + console.error('_utils.storage._defaultStorageObject: Unable to retrieve storage object: ' + e); + } + return storageObject; +} + +/** + ************************************ PSEUDO-PRIVATE METHODS/IVARS ************************************ + * These functions need to be accessible for ease of testing, but should not be used by clients + */ +function _utDefaultStorageObject(storageObjectType) { + return _defaultStorageObject(storageObjectType); +} + +/** + **************************** PUBLIC METHODS/IVARS **************************** + */ +var localStorageObject = _storageObject(_defaultStorageObject(CONSTANTS.STORAGE_TYPE.LOCAL_STORAGE)); +var sessionStorageObject = _storageObject(_defaultStorageObject(CONSTANTS.STORAGE_TYPE.SESSION_STORAGE)); + +/** + * Stringifies an object and saves it to storage + * @param {Object} storageObject an object that adheres to the Web Storage API + * @param {String} key + * @param {Object} (optional) objectToSave the object to stringify and save; if null, key will be removed from storageObject + * @return {Object} the object that was saved to storage or null if nothing was saved (if removing an item, returns undefined) + */ +function saveObjectToStorage(storageObject, key, objectToSave) { + var result = null; + + if (objectToSave) { + // setItem may throw errors if storage is full, or stringify could error + try { + storageObject.setItem(key, JSON.stringify(objectToSave)); + result = objectToSave; + } catch (e) {} + } else { + result = storageObject.removeItem(key); + } + + return result; +} + +/** + * Fetches an object stored as a serialized JSON string and unpacks it + * @param {Object} storageObject an object that adheres to the Web Storage API + * @param {String} key + * @return {Object} the object from storage or null if key doesn't exist in storage (returns undefined if stored value failed to parse) + */ +function objectFromStorage(storageObject, key) { + var result = null; + var serializedObject = storageObject.getItem(key); + + if (serializedObject) { + try { + result = JSON.parse(serializedObject); + } catch (e) { + result = undefined; + } + } + + return result; +} + +var storage = /*#__PURE__*/Object.freeze({ + __proto__: null, + _utDefaultStorageObject: _utDefaultStorageObject, + localStorageObject: localStorageObject, + sessionStorageObject: sessionStorageObject, + saveObjectToStorage: saveObjectToStorage, + objectFromStorage: objectFromStorage +}); + +export { backoff, config, cookies, delegates_info as delegatesInfo, event_fields as eventFields, key_value as keyValue, network, number, reflect, sampling, storage, string }; |
