forked from off-topic/apps.apple.com
2429 lines
105 KiB
JavaScript
2429 lines
105 KiB
JavaScript
/*
|
|
* 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 };
|