mirror of
https://github.com/rxliuli/apps.apple.com.git
synced 2025-11-09 22:10:33 +00:00
3104 lines
119 KiB
JavaScript
3104 lines
119 KiB
JavaScript
import { storage, reflect, string, eventFields } from '@amp-metrics/mt-metricskit-utils-private';
|
|
import { loggerNamed } from '@amp-metrics/mt-client-logger-core';
|
|
|
|
/*
|
|
* src/system/environment.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2017-2018 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* Provides a set of environment-specific (platform-specific) functions which can be individually overridden for the needs
|
|
* of the particular environment, or replaced en masse by providing a single replacement environment delegate object
|
|
* The functionality in this class is typically replaced via a delegate.
|
|
* @see setDelegate
|
|
* @delegatable
|
|
* @constructor
|
|
*/
|
|
var Environment = function Environment() {};
|
|
|
|
/**
|
|
************************************ 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.
|
|
*
|
|
* "setDelegate()" may be called repeatedly, with the functions in the most-recently set delegates replacing any functions matching those in the earlier delegates, as well as any as-yet unreplaced functions.
|
|
* 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, for example, a client wants to use the "canned" itml/environment delegate with the exception of, say, the "appVersion" method, they can set itml/environment as the delegate, and
|
|
* then call "setDelegate()" again with their own delegate containing only a single method of "appVersion" as the delegate, which would leave all the other "replaced" methods intact,
|
|
* but override the "appVersion" method again, this time with their own supplied delegate.
|
|
*
|
|
* NOTE: The delegate function will have a property called origFunction representing the original function that it replaced.
|
|
* This allows the delegate to, essentially, call "super" before or after it does some work.
|
|
* If a replaced method is overridden again with a subsequent "setDelegate()" call, the "origFunction" property will be the previous delegate's function.
|
|
* @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.
|
|
*/
|
|
Environment.prototype.setDelegate = function setDelegate(delegate) {
|
|
return reflect.attachDelegate(this, delegate);
|
|
};
|
|
|
|
/**
|
|
* Some clients have platform-specific implementations of these objects (e.g. iTunes.sessionStorage), so we cover them in case they need to be overriden.
|
|
*/
|
|
Environment.prototype.localStorageObject = storage.localStorageObject;
|
|
Environment.prototype.sessionStorageObject = storage.sessionStorageObject;
|
|
|
|
/**
|
|
* Fetching identifier entity from AMS Metrics Identifier API
|
|
* @param {String} idNamespace - The id namespace that is defined under 'metricsIdentifier' in the bag
|
|
* @param {'userid' | 'clientid'} idType - The identifier type (userid or clientid)
|
|
* @param {Boolean} crossDeviceSync - The boolean flag to indicate whether the identifier is synced across devices
|
|
* @returns {Promise}
|
|
* @overridable
|
|
*/
|
|
Environment.prototype.platformIdentifier = function platformIdentifier(idNamespace, idType, crossDeviceSync) {};
|
|
|
|
/*
|
|
* src/system/index.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2017-2018 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
var System = function System() {
|
|
this.environment = new Environment();
|
|
this.logger = loggerNamed('mt-client-constraints');
|
|
};
|
|
|
|
/*
|
|
* src/utils/key_value.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2023 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* Recursively look up the given keyPath in the provided object.
|
|
* @param {Object} object an object that going to be used for seeking the keyPath
|
|
* @param {String} keyPath a string used to search one or more fields in the object
|
|
* @param {Boolean} inplace a boolean to indicate passing the original/cloned parent to the callback.
|
|
* @param {Function} callback a function will be called when the keyPath has been found in the object
|
|
* @example
|
|
*
|
|
* var target = {
|
|
* aField: {
|
|
* bField: {
|
|
* cArray: [1, 2, 3],
|
|
* dField: 12345
|
|
* },
|
|
* eArray: [{
|
|
* gField: 'hello'
|
|
* }, {
|
|
* gField: 'world'
|
|
* }]
|
|
* }
|
|
* }
|
|
*
|
|
* // loop an object field
|
|
* lookForKeyPath(target, 'aField.bField', false, (value, key, keyPath, object) => {
|
|
* // value = target.aField.bField
|
|
* // key = 'bField'
|
|
* // keyPath = 'aField.bField'
|
|
* // object = target.aField;
|
|
* });
|
|
*
|
|
* // loop an array field
|
|
* lookForKeyPath(target, 'aField.bField.cArray[]', false, (value, key, keyPath, object) => {
|
|
* // Will be called with 3 times with:
|
|
* // value = 1, key = 0, keyPath = aField.bField.cArray[0], object = target.aField.bField.cArray
|
|
* // value = 2, key = 1, keyPath = aField.bField.cArray[1], object = target.aField.bField.cArray
|
|
* // value = 3, key = 2, keyPath = aField.bField.cArray[2], object = target.aField.bField.cArray
|
|
* });
|
|
*
|
|
* // loop a nested field
|
|
* lookForKeyPath(target, 'aField.bField.dField', false, (value, key, keyPath, object) => {
|
|
* // value = 12345
|
|
* // key = 'dField'
|
|
* // keyPath = 'aField.bField.dField';
|
|
* // object = target.aField.bField;
|
|
* });
|
|
*
|
|
* // loop a field of an object in an array field
|
|
* lookForKeyPath(target, 'aField.eArray[].gField',false, (value, key, keyPath, object) => {
|
|
* // Will be called with 2 times with:
|
|
* // value = hello, key = gField, keyPath = aField.eArray[0].gField, object = aField.eArray[0]
|
|
* // value = world, key = gField, keyPath = aField.eArray[1].gField, object = aField.eArray[1]
|
|
* });
|
|
*/
|
|
function lookForKeyPath(object, keyPath, inplace, callback) {
|
|
if (!reflect.isDefined(object) || !reflect.isDefinedNonNullNonEmpty(keyPath) || !reflect.isFunction(callback)) {
|
|
return object;
|
|
}
|
|
var keyPathArray = keyPath.split('.');
|
|
return _lookForKeyPath(object, keyPathArray, null, [], null, inplace, callback);
|
|
}
|
|
|
|
function _lookForKeyPath(object, keyPathArray, key, keyPath, parent, inplace, callback) {
|
|
if (reflect.isFunction(object)) {
|
|
return parent || object;
|
|
}
|
|
|
|
keyPath.push(key);
|
|
|
|
// Handle the leaf fields
|
|
if (keyPathArray.length === 0) {
|
|
callback(object, key, keyPath.slice(1).join('.'), parent);
|
|
return parent || object;
|
|
}
|
|
|
|
if (!reflect.isDefined(object)) {
|
|
return parent || object;
|
|
}
|
|
|
|
var clonedObject = inplace ? object : {};
|
|
var fieldName = keyPathArray.shift();
|
|
// Handle array values
|
|
if (fieldName.length > 2 && fieldName.indexOf('[]') === fieldName.length - 2) {
|
|
fieldName = fieldName.slice(0, -2); // remove []
|
|
keyPath.push(fieldName);
|
|
reflect.extend(clonedObject, object);
|
|
var arrayValue = clonedObject[fieldName];
|
|
if (reflect.isDefinedNonNull(arrayValue)) {
|
|
var processedArray = arrayValue.map(function (arrayItem, i) {
|
|
var updatedArray = inplace ? arrayValue : arrayValue.slice();
|
|
_lookForKeyPath(arrayItem, keyPathArray.slice(), i, keyPath, updatedArray, inplace, callback);
|
|
keyPath.pop();
|
|
return updatedArray[i];
|
|
});
|
|
clonedObject[fieldName] = processedArray;
|
|
}
|
|
} else {
|
|
var fieldValue = object[fieldName];
|
|
reflect.extend(clonedObject, object);
|
|
// Handle normal values
|
|
clonedObject = _lookForKeyPath(fieldValue, keyPathArray, fieldName, keyPath, clonedObject, inplace, callback);
|
|
}
|
|
|
|
keyPath.pop();
|
|
|
|
if (parent) {
|
|
parent[key] = clonedObject;
|
|
return parent;
|
|
} else {
|
|
return clonedObject;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* src/treatment_matchers/nested_fields_match
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2023 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
var MATCH_TYPES_CONFIG = {
|
|
// "MATCH_TYPES_CONFIG.all" is used for checking if all items of the nested fields meet the filter condition.
|
|
all: {
|
|
initMatchValue: true,
|
|
accumulateMatchResult: function (accumulatedResult, matchResult) {
|
|
return accumulatedResult && matchResult;
|
|
}
|
|
},
|
|
// "MATCH_TYPES_CONFIG.any" is used for checking if any of the items of the nested fields meet the filter condition.
|
|
any: {
|
|
initMatchValue: false,
|
|
accumulateMatchResult: function (accumulatedResult, matchResult) {
|
|
return accumulatedResult || matchResult;
|
|
}
|
|
}
|
|
};
|
|
|
|
function getMatchTypeConfig(matchType) {
|
|
var matchConfig = MATCH_TYPES_CONFIG[matchType];
|
|
if (!reflect.isDefinedNonNull(matchConfig)) {
|
|
matchConfig = MATCH_TYPES_CONFIG.all;
|
|
}
|
|
return matchConfig;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {String} fieldName - name of field in eventData
|
|
* @param {Object} eventData - a dictionary of event data
|
|
* @param {Object} matchOptions - an object contains the configurations that related to nested fields and the match options for the compouned matches.
|
|
* @param {Object} matchOptions.matchType - a flag to indicate the match type to apply to the nested fields. Available values "all", "any"
|
|
* @param {Object} matchOptions.matches - an object contains key/value pairs for the actual matches.
|
|
* @returns {Boolean} return true if the field value exists in "fieldMatchValues" otherwise return false
|
|
*/
|
|
function nestedFieldCompoundMatch(fieldName, eventData, matchOptions) {
|
|
if (!reflect.isObject(eventData) || !reflect.isObject(matchOptions)) {
|
|
return false;
|
|
}
|
|
var matchType = matchOptions.matchType;
|
|
var compoundMatches = matchOptions.matches;
|
|
if (!reflect.isDefinedNonNullNonEmpty(compoundMatches)) {
|
|
return false;
|
|
}
|
|
var matchTypeConfig = getMatchTypeConfig(matchType);
|
|
var isMatched = matchTypeConfig.initMatchValue;
|
|
lookForKeyPath(eventData, fieldName, false, function (_value, key, _keyPath, object) {
|
|
var matchResult = Object.keys(compoundMatches).every(function (matcherName) {
|
|
var matcherParam = compoundMatches[matcherName];
|
|
|
|
if (reflect.isDefinedNonNull(matchers[matcherName])) {
|
|
return matchers[matcherName](key, object, matcherParam);
|
|
} else {
|
|
return false;
|
|
}
|
|
});
|
|
isMatched = matchTypeConfig.accumulateMatchResult(isMatched, matchResult);
|
|
});
|
|
|
|
return !!isMatched;
|
|
}
|
|
|
|
/*
|
|
* src/treatment_matchers/non_empty_match
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2020 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
*
|
|
* @param {String} fieldName - name of field in eventData
|
|
* @param {Object} eventData - a dictionary of event data
|
|
* @returns {Boolean} return true if the fieldName does exist in the eventData otherwise return false
|
|
*/
|
|
function nonEmptyMatch(fieldName, eventData) {
|
|
// Since the isObject will return undefined/null if the eventData is undefined/null.
|
|
// workaround here to convert the return value to boolean here to ensure this function returns boolean value. Should be fix it in the isObject()
|
|
return (
|
|
!!reflect.isObject(eventData) &&
|
|
eventData.hasOwnProperty(fieldName) &&
|
|
reflect.isDefinedNonNullNonEmpty(eventData[fieldName])
|
|
);
|
|
}
|
|
|
|
/*
|
|
* src/treatment_matchers/value_match
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2020 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
*
|
|
* @param {String} fieldName - name of field in eventData
|
|
* @param {Object} eventData - a dictionary of event data
|
|
* @param {Array} fieldMatchValues - a list of possible values to match for that field
|
|
* @returns {Boolean} return true if the field value exists in "fieldMatchValues" otherwise return false
|
|
*/
|
|
function valueMatch(fieldName, eventData, fieldMatchValues) {
|
|
if (!reflect.isObject(eventData)) {
|
|
return false;
|
|
}
|
|
|
|
var fieldValue = eventData[fieldName];
|
|
return eventData.hasOwnProperty(fieldName) && fieldMatchValues.indexOf(fieldValue) > -1;
|
|
}
|
|
|
|
/*
|
|
* src/treatment_matchers/non_value_match
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2023 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
*
|
|
* @param {String} fieldName - name of field in eventData
|
|
* @param {Object} eventData - a dictionary of event data
|
|
* @param {Array} fieldNotMatchValues - a list of values to not match for that field
|
|
* @returns {Boolean} return true if the field value do not match ALL the values in "fieldNotMatchValues" otherwise return false
|
|
*/
|
|
function nonValueMatch(fieldName, eventData, fieldNotMatchValues) {
|
|
if (!reflect.isObject(eventData) || !reflect.isArray(fieldNotMatchValues)) {
|
|
return false;
|
|
}
|
|
|
|
var fieldValue = eventData[fieldName];
|
|
return eventData.hasOwnProperty(fieldName) && fieldNotMatchValues.indexOf(fieldValue) === -1;
|
|
}
|
|
|
|
/*
|
|
* src/treatment_matchers/index.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2020 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
var matchers = {
|
|
nonEmpty: nonEmptyMatch,
|
|
valueMatches: valueMatch,
|
|
nonValueMatches: nonValueMatch,
|
|
nestedFieldMatches: nestedFieldCompoundMatch
|
|
};
|
|
|
|
/*
|
|
* src/utils/constants.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2019 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
var FIELD_RULES = {
|
|
OVERRIDE_FIELD_VALUE: 'overrideFieldValue'
|
|
};
|
|
|
|
/*
|
|
* src/field_handlers/base.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2017-2018 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* Provides methods to manage field constraints that apply to all fields
|
|
* @constructor
|
|
*/
|
|
var Base = function Base() {};
|
|
|
|
/**
|
|
************************************ PUBLIC METHODS ************************************
|
|
*/
|
|
/**
|
|
* @param {Object} eventFields a dictionary of event data
|
|
* @param {Object} fieldRules includes information about how to constrain a field
|
|
* @param {String} fieldName the name of the field to constrain
|
|
* @return {any} a field value that adheres to the provided rules
|
|
* @overridable
|
|
*/
|
|
Base.prototype.constrainedValue = function constrainedValue(eventFields, fieldRules, fieldName) {
|
|
var fieldValue = eventFields && eventFields.hasOwnProperty(fieldName) ? eventFields[fieldName] : null;
|
|
return this.applyConstraintRules(fieldValue, fieldRules);
|
|
};
|
|
|
|
/**
|
|
* @param {any} fieldValue an unconstrained value
|
|
* @param {Object} fieldRules includes information about how to constrain a field
|
|
* @return {any} a field value that adheres to the provided rules
|
|
* @overridable
|
|
*/
|
|
Base.prototype.applyConstraintRules = function applyConstraintRules(fieldValue, fieldRules) {
|
|
var returnValue = fieldValue;
|
|
if (fieldRules) {
|
|
var denylisted = fieldRules.denylisted || fieldRules.blacklisted;
|
|
if (denylisted) {
|
|
returnValue = null;
|
|
} else if (fieldRules.hasOwnProperty(FIELD_RULES.OVERRIDE_FIELD_VALUE)) {
|
|
returnValue = fieldRules.overrideFieldValue;
|
|
}
|
|
}
|
|
return returnValue;
|
|
};
|
|
|
|
/*
|
|
* src/field_actions/base.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2020 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
var exceptionString = string.exceptionString;
|
|
|
|
/**
|
|
* Parent class of field_actions classes
|
|
* @param {Object} constraintsInstance - the instance of Constraints class
|
|
* @constructor
|
|
*/
|
|
var Base$1 = function Base(constraintsInstance) {
|
|
// @private
|
|
this._constraintsInstance = constraintsInstance;
|
|
};
|
|
|
|
/**
|
|
************************************ 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.
|
|
*
|
|
* "setDelegate()" may be called repeatedly, with the functions in the most-recently set delegates replacing any functions matching those in the earlier delegates, as well as any as-yet unreplaced functions.
|
|
* 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, for example, a client wants to use the "canned" itml/environment delegate with the exception of, say, the "appVersion" method, they can set itml/environment as the delegate, and
|
|
* then call "setDelegate()" again with their own delegate containing only a single method of "appVersion" as the delegate, which would leave all the other "replaced" methods intact,
|
|
* but override the "appVersion" method again, this time with their own supplied delegate.
|
|
*
|
|
* NOTE: The delegate function will have a property called origFunction representing the original function that it replaced.
|
|
* This allows the delegate to, essentially, call "super" before or after it does some work.
|
|
* If a replaced method is overridden again with a subsequent "setDelegate()" call, the "origFunction" property will be the previous delegate's function.
|
|
* @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.
|
|
*/
|
|
Base$1.prototype.setDelegate = function setDelegate(delegate) {
|
|
return reflect.attachDelegate(this, delegate);
|
|
};
|
|
|
|
/**
|
|
* Abstract method to constrain value
|
|
* @param {Any} value - the value of fieldName in eventData, null if the eventData does not exist or the fieldName does not exist in the eventData
|
|
* @param {Object} fieldRules - includes information about how to constrain the field
|
|
* @param {Object} eventData - a dictionary of event data, which should include a pre-existing (unconstrained) field
|
|
* @param {String} fieldName - field name/path that can locate the "value" parameter in eventData
|
|
* @return {Any} the constrained value
|
|
*/
|
|
Base$1.prototype.constrainedValue = function constrainedValue(value, fieldRules, eventData, fieldName) {
|
|
throw exceptionString('field_actions.Base', 'constrainedValue');
|
|
};
|
|
|
|
/**
|
|
* A public method to wrap the "constrainedValue" method to contains common code for all field_actions subclasses.
|
|
* @param {Any} value - field value in eventData that is performed by field_actions
|
|
* @param {String} fieldName - field name/path that can locate the "value" parameter in eventData
|
|
* @param {Object} eventData - a dictionary of event data, which should include a pre-existing (unconstrained) field
|
|
* @param {Object} fieldRules - includes information about how to constrain the field
|
|
* @return {Any} the constrained value
|
|
*/
|
|
Base$1.prototype.performAction = function performAction(value, fieldName, eventData, fieldRules) {
|
|
// return the original value if there are no rules to apply
|
|
if (reflect.isDefinedNonNull(fieldRules) && !reflect.isEmptyObject(fieldRules)) {
|
|
value = this.constrainedValue(value, fieldRules, eventData, fieldName);
|
|
}
|
|
return value;
|
|
};
|
|
|
|
/*
|
|
* src/utils/url.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2017-2018 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
************************************ PUBLIC METHODS/IVARS ************************************
|
|
*/
|
|
|
|
/**
|
|
* @param {String} aUrl
|
|
* @return {String} the hostname part of the provided url e.g. www.apple.com
|
|
* @overridable
|
|
* Note: see https://nodejs.org/api/url.html for a diagram illustrating the different parts of a URL
|
|
*/
|
|
function hostname(aUrl) {
|
|
aUrl = aUrl || '';
|
|
|
|
var urlParts = withoutParams(aUrl).split('/');
|
|
var hostAndAuth;
|
|
var host;
|
|
var hostname;
|
|
|
|
if (aUrl.indexOf('//') === -1) {
|
|
hostAndAuth = urlParts[0];
|
|
} else {
|
|
hostAndAuth = urlParts[2];
|
|
}
|
|
|
|
host = hostAndAuth.substring(hostAndAuth.indexOf('@') + 1);
|
|
hostname = host.split(':')[0];
|
|
|
|
return hostname;
|
|
}
|
|
|
|
/**
|
|
* @param {String} aUrl
|
|
* @return {String} the domain part of the provided url shortened to the "main" part of the domain (apple.com or apple.co.uk)
|
|
* @overridable
|
|
* Note: this method uses a heuristic to determine if the url has a country code second level domain and will miss
|
|
* ccSLDs that are not exactly two chracters long or one of: 'com', 'org, 'net', edu', 'gov'
|
|
* For example, "www.example.ltd.uk" will be shortened to "ltd.uk"
|
|
* All two-letter top-level domains are ccTLDs: https://en.wikipedia.org/wiki/Country_code_top-level_domain
|
|
*/
|
|
function mainDomain(aUrl) {
|
|
var urlSegments = hostname(aUrl).split('.');
|
|
var lastSegment = urlSegments[urlSegments.length - 1];
|
|
var secondToLastSegment = urlSegments[urlSegments.length - 2];
|
|
var segmentsToKeep = 2;
|
|
|
|
if (
|
|
lastSegment &&
|
|
lastSegment.length === 2 &&
|
|
secondToLastSegment &&
|
|
(secondToLastSegment.length === 2 || secondToLastSegment in reservedCCSLDs())
|
|
) {
|
|
segmentsToKeep = 3;
|
|
}
|
|
|
|
return urlSegments.slice(-1 * segmentsToKeep).join('.');
|
|
}
|
|
|
|
/**
|
|
* @return {Object} a map of country-code second level domains (ccSLDs) used in the heuristic
|
|
* that determines the main part of a domain (defined as TLD + ccSLD + 1)
|
|
* @overridable
|
|
*/
|
|
function reservedCCSLDs() {
|
|
var reservedCCSLDs = {
|
|
com: true,
|
|
org: true,
|
|
net: true,
|
|
edu: true,
|
|
gov: true
|
|
};
|
|
|
|
return reservedCCSLDs;
|
|
}
|
|
|
|
/**
|
|
* @param {String} aUrl
|
|
* @param {Array|Object} allowedParams An array of allowed params or an object with each param containing its allowed values
|
|
* @return {String} the url with any disallowed query parameters and/or hash removed
|
|
* @overridable
|
|
*
|
|
* @example
|
|
* withoutParams('https://itunes.apple.com/?p1=10&p2=hello', ['p1']);
|
|
* // returns 'https://itunes.apple.com/?p1=10'
|
|
*
|
|
* withoutParams('https://itunes.apple.com/?p1=10&p2=hello', {
|
|
* p1: {
|
|
* allowedValues: ['20', '30']
|
|
* },
|
|
* . p2: {
|
|
* allowedValues: ['hello']
|
|
* }
|
|
* });
|
|
* // returns 'https://itunes.apple.com/?p2=hello'
|
|
*/
|
|
function withoutParams(aUrl, allowedParams) {
|
|
var url = aUrl || '';
|
|
var urlParts = url.split('?');
|
|
var urlPrefix = urlParts[0];
|
|
var urlParams = withoutHash(urlParts[1]).split('&');
|
|
|
|
var filteredParams = urlParams
|
|
.filter(function (paramString) {
|
|
var keyAndVal = paramString.split('=');
|
|
var paramName = keyAndVal[0];
|
|
var paramVal = keyAndVal[1];
|
|
|
|
if (reflect.isArray(allowedParams)) {
|
|
return allowedParams.indexOf(paramName) !== -1;
|
|
}
|
|
|
|
if (reflect.isObject(allowedParams)) {
|
|
return (
|
|
reflect.isObject(allowedParams[paramName]) &&
|
|
reflect.isArray(allowedParams[paramName].allowedValues) &&
|
|
allowedParams[paramName].allowedValues.indexOf(paramVal) !== -1
|
|
);
|
|
}
|
|
|
|
return false;
|
|
})
|
|
.join('&');
|
|
|
|
return filteredParams.length > 0 ? urlPrefix + '?' + filteredParams : urlPrefix;
|
|
}
|
|
|
|
/**
|
|
* Returns the url with the hash removed
|
|
* @param {String} aUrl
|
|
* @return {String} the url with any hash removed
|
|
* @overridable
|
|
* @example
|
|
* withoutHash('https://itunes.apple.com:80/music?param1=abc¶m2=def¶m3=ghi#someHash')
|
|
* // returns 'https://itunes.apple.com:80/music?param1=abc¶m2=def¶m3=ghi'
|
|
*/
|
|
function withoutHash(aUrl) {
|
|
var url = aUrl || '';
|
|
return url.split('#')[0];
|
|
}
|
|
|
|
/**
|
|
* Returns the url with all of the string replacements applied
|
|
* @param {String} aUrl
|
|
* @param {Object} replacements The list of replacements that should be applied on the url
|
|
* @param {String} replacements.searchPattern A stringified regex pattern to search for
|
|
* @param {String} replacements.replaceVal The string to replace the match with
|
|
* @param {String} replacements.flags Regex flags to include with the search pattern (ex: 'g')
|
|
* @return {String} The url with all replacements applied
|
|
* @overridable
|
|
*
|
|
* @example
|
|
* withReplacements('https://itunes.apple.com:80/music?param1=abc¶m2=def¶m3=ghi#someHash', [
|
|
* {
|
|
* searchPattern: 'music',
|
|
* replaceVal: 'm'
|
|
* }
|
|
* ])
|
|
* // returns 'https://itunes.apple.com:80/m?param1=abc¶m2=def¶m3=ghi#someHash'
|
|
*
|
|
* withReplacements('https://apple.com/1234', [
|
|
* {
|
|
* searchPattern: '\d',
|
|
* replaceVal: 'X',
|
|
* flags: 'g'
|
|
* }
|
|
* ])
|
|
* // returns 'https://apple.com/XXXX'
|
|
*/
|
|
function withReplacements(aUrl, replacements) {
|
|
var url = aUrl || '';
|
|
var urlReplacements = replacements || [];
|
|
|
|
var replacedUrl = urlReplacements.reduce(function (url, replacement) {
|
|
var searchPattern = new RegExp(replacement.searchPattern, replacement.flags);
|
|
var replaceVal = replacement.replaceVal;
|
|
return url.replace(searchPattern, replaceVal);
|
|
}, url);
|
|
|
|
return replacedUrl;
|
|
}
|
|
|
|
/*
|
|
* src/utils/id_generator.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2023 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
var ID_SEPARATOR = '-';
|
|
var DEFAULT_GENERATED_ID_SEPARATOR = 'z';
|
|
|
|
/**
|
|
* @param {Object} options includes - information about how to generate an ID
|
|
* @param {Number} options.idVersion - the version of the ID
|
|
* @param {Number} options.time - the time to be a part of the ID (optional)
|
|
* @param {String} options.generatedIdSeparator - a token-separated hex string of metadata to attach to a ID (optional) default to "z"
|
|
* @return {String} a generated ID
|
|
*/
|
|
function generateId(options) {
|
|
if (!reflect.isDefinedNonNull(options) || !reflect.isInteger(options.idVersion)) {
|
|
return '0';
|
|
}
|
|
var uuid = string.uuid();
|
|
var generatedIdSeparator = options.generatedIdSeparator || DEFAULT_GENERATED_ID_SEPARATOR;
|
|
var idString = generatedIdMetadata(options) + ID_SEPARATOR + uuid || '';
|
|
|
|
var convertedIdString = idString
|
|
.split(ID_SEPARATOR)
|
|
.map(function (segment) {
|
|
var segmentAsNumber = parseInt(segment, 16);
|
|
return string.convertNumberToBaseAlphabet(segmentAsNumber, string.base61Alphabet);
|
|
})
|
|
.join(generatedIdSeparator);
|
|
|
|
return convertedIdString;
|
|
}
|
|
|
|
/**
|
|
* @param {Object} options includes - information about how to generate an ID
|
|
* @param {Number} options.idVersion - the version of the ID
|
|
* @param {Number} options.time - the time to be a part of the ID (optional)
|
|
* @return {String} a token-separated hex string of metadata to attach to a ID,
|
|
*/
|
|
function generatedIdMetadata(options) {
|
|
var parameters = [options.idVersion];
|
|
|
|
if (options.time) {
|
|
parameters.push(options.time);
|
|
}
|
|
|
|
return parameters
|
|
.map(function (param) {
|
|
return param.toString(16);
|
|
})
|
|
.join(ID_SEPARATOR);
|
|
}
|
|
|
|
/*
|
|
* src/field_actions/id_action/time_based_action.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2021 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* The time-based ID generating strategy
|
|
* The ID value will change after its lifespan expires
|
|
*/
|
|
function constrainedValue(idString, idRules, eventData, fieldName) {
|
|
var storageKey = this.storageKey(fieldName, eventData, idRules);
|
|
var environment = this._constraintsInstance.system.environment;
|
|
var idData = storage.objectFromStorage(environment.localStorageObject(), storageKey) || {};
|
|
|
|
idData.value = this.idString(idData, idRules);
|
|
if (
|
|
this.rulesHaveLifespan(idRules) &&
|
|
(!reflect.isNumber(idData.expirationTime) || this.timeExpired(idData.expirationTime))
|
|
) {
|
|
idData.expirationTime = this.expirationTime(idRules.lifespan);
|
|
}
|
|
|
|
storage.saveObjectToStorage(environment.localStorageObject(), storageKey, idData);
|
|
idString = idData.value;
|
|
|
|
return idString;
|
|
}
|
|
|
|
/*
|
|
* src/field_actions/id_action/session_time_based_action.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2021 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
// @private
|
|
// A global cache storage to store the served clientIds by the clientId storageKey
|
|
// Make it as a global variable to ensure the cached clientId can be shared between multiple MK instances
|
|
var _sessionIdCache = {};
|
|
|
|
/**
|
|
* The user-session-based + time-based ID generating strategy
|
|
* When the id getting expired, this function will return a consistent ID until the current user session ends, even if the ID is scheduled to expire in the middle of the session
|
|
*/
|
|
function constrainedValue$1(idString, idRules, eventData, fieldName) {
|
|
var storageKey = this.storageKey(fieldName, eventData, idRules);
|
|
var returnedIdString = _sessionIdCache[storageKey];
|
|
|
|
if (!returnedIdString) {
|
|
returnedIdString = constrainedValue.apply(this, arguments);
|
|
_sessionIdCache[storageKey] = returnedIdString;
|
|
}
|
|
return returnedIdString;
|
|
}
|
|
|
|
/*
|
|
* src/field_actions/id_action/id_action.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2020 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
var STORAGE_KEY_SEPARATOR = '_';
|
|
var MT_ID_NAMESPACE = 'mtId';
|
|
|
|
var IdAction = function IdAction() {
|
|
Base$1.apply(this, arguments);
|
|
};
|
|
|
|
IdAction.prototype = Object.create(Base$1.prototype);
|
|
IdAction.prototype.constructor = IdAction;
|
|
|
|
/*
|
|
* Possible strategies that can be used to scope an ID value
|
|
*/
|
|
IdAction.prototype.SCOPE_STRATEGIES = {
|
|
ALL: 'all',
|
|
MAIN_DOMAIN: 'mainDomain'
|
|
};
|
|
|
|
/**
|
|
* @param {Object} (optional) idRules includes information about when to expire the ID
|
|
* @return {Boolean}
|
|
*/
|
|
IdAction.prototype.rulesHaveLifespan = function rulesHaveLifespan(idRules) {
|
|
idRules = idRules || {};
|
|
|
|
return reflect.isNumber(idRules.lifespan);
|
|
};
|
|
|
|
/**
|
|
* @param {Number} (optional) lifespan the amount of time, in milliseconds, that an ID should be valid for
|
|
* @return {Number} a timestamp in ms since epoch, or null if no lifespan was provided
|
|
*/
|
|
IdAction.prototype.expirationTime = function expirationTime(lifespan) {
|
|
return lifespan ? Date.now() + lifespan : null;
|
|
};
|
|
|
|
/**
|
|
* @param {String} fieldName - name of the field being field_actions in eventData
|
|
* @param {Object} eventData a dictionary of event data
|
|
* @param {Object} idRules includes information about how to namespace/scope the id
|
|
* @return {String} the key that id data should be stored under
|
|
* @example
|
|
* (storageKeyPrefix ? storageKeyPrefix : mtId_<fieldName>)_<namespace>_(scopeStrategy ? <eventData[scopeFieldName]> : 'all')
|
|
* @overridable
|
|
*/
|
|
IdAction.prototype.storageKey = function storageKey(fieldName, eventData, idRules) {
|
|
var scope = this.scope(eventData, idRules);
|
|
return this.storageKeyPrefix(idRules, fieldName) + (scope ? STORAGE_KEY_SEPARATOR + scope : '');
|
|
};
|
|
|
|
/**
|
|
* @param {Object} idRules includes information about how to namespace/scope the id
|
|
* @param {String} fieldName - name of the field being field_actions in eventData
|
|
* @return {String} a prefix to be used when storing id data in localStorage
|
|
* @overridable
|
|
*/
|
|
IdAction.prototype.storageKeyPrefix = function storageKeyPrefix(idRules, fieldName) {
|
|
return idRules && reflect.isString(idRules.storageKeyPrefix) && idRules.storageKeyPrefix.length > 0
|
|
? idRules.storageKeyPrefix
|
|
: MT_ID_NAMESPACE + STORAGE_KEY_SEPARATOR + fieldName;
|
|
};
|
|
|
|
/**
|
|
* @param {Object} eventData a dictionary of event data
|
|
* @param {Object} idRules includes information about how to namespace/scope the id
|
|
* @return {String} the namespace/scope for this set of event data and rules
|
|
* @overridable
|
|
*/
|
|
IdAction.prototype.scope = function scope(eventData, idRules) {
|
|
var idKey = '';
|
|
|
|
if (idRules) {
|
|
if (idRules.namespace) {
|
|
idKey += idRules.namespace;
|
|
}
|
|
if (idRules.scopeStrategy) {
|
|
var domainScope;
|
|
|
|
switch (idRules.scopeStrategy) {
|
|
case this.SCOPE_STRATEGIES.MAIN_DOMAIN:
|
|
var scopeFieldName = idRules.scopeFieldName;
|
|
domainScope = mainDomain(eventData[scopeFieldName]) || 'unknownDomain';
|
|
break;
|
|
case this.SCOPE_STRATEGIES.ALL: /* fall through */
|
|
default:
|
|
// no scope
|
|
domainScope = this.SCOPE_STRATEGIES.ALL;
|
|
break;
|
|
}
|
|
|
|
if (idKey.length) {
|
|
idKey += STORAGE_KEY_SEPARATOR;
|
|
}
|
|
idKey += domainScope;
|
|
}
|
|
}
|
|
|
|
return idKey;
|
|
};
|
|
|
|
/**
|
|
* @param {Object} (optional) existingIdData
|
|
* @param {Object} (optional) idRules includes information about when to expire the ID
|
|
* @return {String} an ID
|
|
* @overridable
|
|
*/
|
|
IdAction.prototype.idString = function idString(existingIdData, idRules) {
|
|
var existingId = existingIdData ? existingIdData.value : null;
|
|
var returnValue = existingId;
|
|
|
|
if (
|
|
!existingId ||
|
|
(reflect.isNumber(existingIdData.expirationTime) && this.timeExpired(existingIdData.expirationTime))
|
|
) {
|
|
returnValue = this.generateId(idRules);
|
|
}
|
|
|
|
return returnValue;
|
|
};
|
|
|
|
/**
|
|
* @param {Object} (optional) idRules includes information about how to constrain the field
|
|
* @return {String} a generated ID
|
|
* @overridable
|
|
* @see comments in the related MTClientId.java
|
|
*/
|
|
IdAction.prototype.generateId = function generateId$1(idRules) {
|
|
idRules = idRules || {};
|
|
return generateId({
|
|
idVersion: this.generatedIdVersion(),
|
|
time: this.expirationTime(idRules.lifespan),
|
|
generatedIdSeparator: this.generatedIdSeparator(idRules.tokenSeparator)
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @return {Number} the version of the generated ID
|
|
* @overridable
|
|
*/
|
|
IdAction.prototype.generatedIdVersion = function generatedIdVersion() {
|
|
return 4;
|
|
};
|
|
|
|
/**
|
|
* @return {String} the separator used to tokenize sections of an unformatted ID string
|
|
* @overridable
|
|
*/
|
|
IdAction.prototype.idTokenSeparator = function idTokenSeparator() {
|
|
return '-';
|
|
};
|
|
|
|
/**
|
|
* @param {String} (optional) separator
|
|
* @return {String} the separator used to tokenize sections of a finalized, formatted ID string
|
|
* @overridable
|
|
*/
|
|
IdAction.prototype.generatedIdSeparator = function generatedIdSeparator(separator) {
|
|
return separator || 'z';
|
|
};
|
|
|
|
/**
|
|
* @param {Number} timestamp a timestamp in ms since epoch
|
|
* @return {Boolean}
|
|
* @overridable
|
|
*/
|
|
IdAction.prototype.timeExpired = function timeExpired(timestamp) {
|
|
return timestamp <= Date.now();
|
|
};
|
|
|
|
/**
|
|
* @param {String} idString - the ID field in eventData
|
|
* @param {Object} idRules - includes information about how to constrain the field
|
|
* @param {String}(optional) idRules.storageKeyPrefix - a prefix to be used when storing ID data in localStorage, default is MT_ID_NAMESPACE
|
|
* @param {String}(optional) idRules.namespace - a string to be used when storing ID data in localStorage.
|
|
* @param {String}(optional) idRules.scopeStrategy - a strategy that can be used to scope a ID value [all/mainDomain]
|
|
* @param {String}(optional) idRules.scopeFieldName - name of the scope field in eventData, the value would be an URL and used to get the main domain as a part of scope. It is used when parameters.scopeStrategy set to "mainDomain"
|
|
* @param {String}(optional) idRules.tokenSeparator - the separator used to tokenize sections of a finalized, formatted ID string. Default is 'z'
|
|
* @param {Integer}(optional) idRules.lifespan - the expiration period for the ID (milliseconds)
|
|
* @param {Boolean}(optional) idRules.persistIdForSession - a boolean to indicate whether to persist the ID until the current user session ends, even if it is scheduled to expire in the middle of the session
|
|
* @param {Object} eventData - a dictionary of event data, which should include a pre-existing (unconstrained) ID field
|
|
* @param {String} fieldName - name of the field being field_actions in eventData
|
|
* @return {String} the constrained ID
|
|
*/
|
|
IdAction.prototype.constrainedValue = function constrainedValue$2(idString, idRules, eventData, fieldName) {
|
|
if (eventData && idRules && !reflect.isEmptyObject(idRules)) {
|
|
if (idRules.persistIdForSession === true) {
|
|
idString = constrainedValue$1.apply(this, arguments);
|
|
} else {
|
|
idString = constrainedValue.apply(this, arguments);
|
|
}
|
|
}
|
|
return idString;
|
|
};
|
|
|
|
/*
|
|
* src/field_handlers/client_id.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2017-2018 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* Provides methods to manage clientId field constraints.
|
|
* @constructor
|
|
*/
|
|
var ClientId = function ClientId(base, constraintsInstance) {
|
|
// @private
|
|
this._base = base;
|
|
// @private
|
|
this._idAction = new IdAction(constraintsInstance);
|
|
this._idAction.setDelegate({
|
|
storageKey: function storageKey(fieldName, eventData, idRules) {
|
|
return this.storageKeyPrefix() + '_' + this.scope(eventData, idRules);
|
|
}.bind(this._idAction),
|
|
storageKeyPrefix: function storageKeyPrefix() {
|
|
return 'mtClientId';
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
************************************ PUBLIC METHODS/IVARS ************************************
|
|
*/
|
|
|
|
/**
|
|
* @param {Object} eventFields a dictionary of event data, which may include a pre-existing (unconstrained) clientId
|
|
* @param {Object} clientIdRules includes information about when to expire the clientId and how to namespace/scope it
|
|
* @return {String} a clientId that adheres to the provided rules
|
|
* @overridable
|
|
*/
|
|
ClientId.prototype.constrainedValue = function constrainedValue(eventFields, clientIdRules) {
|
|
// adapt expirationPeriod to lifespan
|
|
var clonedRules = clientIdRules;
|
|
if (clientIdRules && reflect.isNumber(clientIdRules.expirationPeriod)) {
|
|
clonedRules = reflect.extend({}, clientIdRules);
|
|
clonedRules.lifespan = clonedRules.expirationPeriod;
|
|
delete clonedRules.expirationPeriod;
|
|
}
|
|
var clientId = eventFields ? eventFields.clientId : null;
|
|
var clientIdString = this._idAction.performAction(clientId, 'clientId', eventFields, clonedRules);
|
|
|
|
return this._base.applyConstraintRules(clientIdString, clientIdRules);
|
|
};
|
|
|
|
/*
|
|
* src/field_actions/url_action.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2020 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
var UrlAction = function UrlAction() {
|
|
Base$1.apply(this, arguments);
|
|
};
|
|
|
|
UrlAction.prototype = Object.create(Base$1.prototype);
|
|
UrlAction.prototype.constructor = UrlAction;
|
|
|
|
/*
|
|
* Possible truncation strategies that can be applied to a parentPageUrl
|
|
*/
|
|
UrlAction.prototype.SCOPES = {
|
|
HOSTNAME: 'hostname',
|
|
FULL: 'full',
|
|
FULL_WITHOUT_PARAMS: 'fullWithoutParams',
|
|
FULL_WITH_REPLACEMENTS: 'fullWithReplacements'
|
|
};
|
|
|
|
/**
|
|
* @param {String} url - the URL field in eventData
|
|
* @param {Object} fieldRules - includes information about how to constrain the field
|
|
* @return {String} the constrained URL
|
|
*/
|
|
UrlAction.prototype.constrainedValue = function constrainedValue(url, fieldRules) {
|
|
if (url && fieldRules && fieldRules.scope) {
|
|
switch (fieldRules.scope) {
|
|
case this.SCOPES.HOSTNAME:
|
|
url = hostname(url);
|
|
break;
|
|
case this.SCOPES.FULL_WITHOUT_PARAMS:
|
|
url = withoutParams(url, fieldRules.allowedParams);
|
|
break;
|
|
case this.SCOPES.FULL_WITH_REPLACEMENTS:
|
|
url = withReplacements(url, fieldRules.replacements);
|
|
break;
|
|
case this.SCOPES.FULL: /* fall through */
|
|
}
|
|
}
|
|
|
|
return url;
|
|
};
|
|
|
|
/*
|
|
* src/field_handlers/parent_page_url.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2017-2018 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* Provides methods to manage parentPageUrl field constraints.
|
|
* @constructor
|
|
*/
|
|
var ParentPageUrl = function ParentPageUrl(base) {
|
|
// @private
|
|
this._base = base;
|
|
// @private
|
|
this._urlAction = new UrlAction();
|
|
};
|
|
|
|
/**
|
|
************************************ PUBLIC METHODS/IVARS ************************************
|
|
*/
|
|
|
|
/**
|
|
* @param {Object} eventFields a dictionary of event data, which should include a pre-existing (unconstrained) parentPageUrl
|
|
* @param {Object} parentPageUrlRules includes information about whether to strip certain parts of the URL
|
|
* @return {String} a parentPageUrl modified according to the provided rules
|
|
* @overridable
|
|
*/
|
|
ParentPageUrl.prototype.constrainedValue = function constrainedValue(eventFields, parentPageUrlRules) {
|
|
var parentPageUrl = eventFields ? eventFields.parentPageUrl : null;
|
|
var modifiedUrl = this._urlAction.performAction(parentPageUrl, 'parentPageUrl', eventFields, parentPageUrlRules);
|
|
return this._base.applyConstraintRules(modifiedUrl, parentPageUrlRules);
|
|
};
|
|
|
|
/*
|
|
* src/field_handlers/index.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2017-2018 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* @deprecated the field handlers has been deprecated and replaced with "De-res treatments(src/field_actions/*)"
|
|
* @param constraintsInstance
|
|
* @constructor
|
|
*/
|
|
var FieldHandlers = function (constraintsInstance) {
|
|
this.base = new Base(constraintsInstance);
|
|
this.clientId = new ClientId(this.base, constraintsInstance);
|
|
this.parentPageUrl = new ParentPageUrl(this.base, constraintsInstance);
|
|
};
|
|
|
|
/*
|
|
* src/treatment/legacy_treatment.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2020 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
var LegacyTreatment = function LegacyTreatment(constraintInstance) {
|
|
// @private
|
|
this._fieldHandlers = new FieldHandlers(constraintInstance);
|
|
};
|
|
|
|
/**
|
|
* @param {Object} eventData a dictionary of event data
|
|
* @param {Object} eventConstraints a set of constraints to apply to this event
|
|
* @return {Object} the event data modified according to the appropriate constraints
|
|
* Note: event fields will be modified in place and also returned
|
|
* @example
|
|
* var eventData = {
|
|
* eventType: 'click',
|
|
* pageType: 'TopCharts',
|
|
* parentPageUrl: 'https://itunes.apple.com/music/topcharts/12345',
|
|
* // etc.
|
|
* };
|
|
* var eventConstraints = {
|
|
* fieldConstraints: { parentPageUrl: { scope: 'hostname' } }
|
|
* }
|
|
* legacyTreatment.applyConstraints(eventData, eventConstraints) =>
|
|
* {
|
|
* eventType: click,
|
|
* pageType: 'TopCharts',
|
|
* parentPageUrl: 'itunes.apple.com', // truncated to hostname only
|
|
* etc... // all other fields remain the same
|
|
* }
|
|
*/
|
|
LegacyTreatment.prototype.applyConstraints = function applyConstraints(eventData, eventConstraints) {
|
|
if (eventConstraints && eventConstraints.fieldConstraints) {
|
|
eventData = this.applyFieldConstraints(eventData, eventConstraints.fieldConstraints);
|
|
}
|
|
|
|
return eventData;
|
|
};
|
|
|
|
/**
|
|
* @param {Object} eventData a dictionary of event data
|
|
* @param {Object} fieldConstraints a set of constraints to apply to fields in this event, keyed by field name
|
|
* @return {Object} the event data modified according to the appropriate constraints
|
|
* Note: event fields will be modified in place and also returned
|
|
*/
|
|
LegacyTreatment.prototype.applyFieldConstraints = function applyFieldConstraints(eventData, fieldConstraints) {
|
|
if (fieldConstraints) {
|
|
var constrainedFieldValues = {};
|
|
var constrainedValue;
|
|
var fieldRules;
|
|
var fieldName;
|
|
|
|
for (fieldName in fieldConstraints) {
|
|
fieldRules = fieldConstraints[fieldName];
|
|
if (
|
|
eventData.hasOwnProperty(fieldName) ||
|
|
fieldRules.generateValue === true ||
|
|
fieldRules.hasOwnProperty(FIELD_RULES.OVERRIDE_FIELD_VALUE)
|
|
) {
|
|
if (fieldName in this._fieldHandlers) {
|
|
constrainedValue = this._fieldHandlers[fieldName].constrainedValue(eventData, fieldRules);
|
|
} else {
|
|
constrainedValue = this._fieldHandlers.base.constrainedValue(eventData, fieldRules, fieldName);
|
|
}
|
|
|
|
constrainedFieldValues[fieldName] = constrainedValue;
|
|
}
|
|
}
|
|
|
|
// assign constrained values only after all of them have been calculated
|
|
// in case some constrained values depend on more than one original field values
|
|
for (fieldName in constrainedFieldValues) {
|
|
eventData[fieldName] = constrainedFieldValues[fieldName];
|
|
}
|
|
|
|
eventData = eventFields.mergeAndCleanEventFields(eventData);
|
|
}
|
|
|
|
return eventData;
|
|
};
|
|
|
|
/*
|
|
* src/utils/constraint_generator
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2021 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* Adding/replacing the rule properties of targetRules with the ones of newRules
|
|
* @param {Object} targetRules - an object contains an <fieldConstraintsName> property with field rules in it.
|
|
* @param {Object} newRules - an object contains an <fieldConstraintsName> property with new field rules in it.
|
|
* @param {String} fieldRulesKeyName - the field rule property name
|
|
* @param {Function} initialFieldRules - a callback function to decide the target field rule object. Function signature: function (targetRules, sourceRules, fieldName)
|
|
* @returns {Object} updated target rules, all replacements are in-place updating unless the passed-in targetRules object does not exist
|
|
*/
|
|
function updateFieldRulesets(targetRules, newRules, fieldRulesKeyName, initialFieldRules) {
|
|
var updatedRules = targetRules || {};
|
|
initialFieldRules =
|
|
initialFieldRules ||
|
|
function (targetRules, sourceRules, fieldName) {
|
|
return targetRules[fieldName] || {};
|
|
};
|
|
|
|
if (newRules && newRules[fieldRulesKeyName]) {
|
|
var fieldName;
|
|
var propertyName;
|
|
var updatedFieldRules = updatedRules[fieldRulesKeyName] || {};
|
|
updatedRules[fieldRulesKeyName] = updatedFieldRules;
|
|
|
|
// copying the top level rules over is sufficient for a simple rule structure
|
|
for (fieldName in newRules[fieldRulesKeyName]) {
|
|
var fieldRules = initialFieldRules(updatedFieldRules, newRules[fieldRulesKeyName], fieldName);
|
|
updatedFieldRules[fieldName] = fieldRules;
|
|
for (propertyName in newRules[fieldRulesKeyName][fieldName]) {
|
|
fieldRules[propertyName] = newRules[fieldRulesKeyName][fieldName][propertyName];
|
|
}
|
|
}
|
|
}
|
|
|
|
return updatedRules;
|
|
}
|
|
|
|
/*
|
|
* src/constraint_generator/legacy_constraint_generator.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2017-2018 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
var LegacyConstraintGenerator = function LegacyConstraintGenerator(constraintInstance) {
|
|
this.treatment = new LegacyTreatment(constraintInstance);
|
|
};
|
|
|
|
/**
|
|
* @param {Object} eventData a dictionary of event data
|
|
* @param {Object} topicConfig the AMP Metrics topic config to use to look up constraint_generator values
|
|
* @param {String}(optional) topic defines the AMP Analytics "topic" to look up the constraint profile
|
|
* @return {Object} a set of constraints to apply to this event
|
|
* @overridable
|
|
* Constraint rules will be applied in the order they are provided in config.
|
|
* @example
|
|
* Given the following config:
|
|
* { constraints: {
|
|
* defaultProfile: 'strict',
|
|
* profiles: {
|
|
* strict: {
|
|
* precedenceOrderedRules: [
|
|
* {
|
|
* filters: 'any',
|
|
* fieldConstraints: {
|
|
* clientId: {
|
|
* generateValue: true,
|
|
* tokenSeparator: 'z',
|
|
* scopeFieldName: 'parentPageUrl',
|
|
* scopeStrategy: 'mainDomain', // apple.com or apple.co.uk
|
|
* expirationPeriod: 86400000 // 24h
|
|
* },
|
|
* parentPageUrl: {
|
|
* scope: 'hostname' // www.apple.com
|
|
* }
|
|
* }
|
|
* },
|
|
* {
|
|
* filters: {
|
|
* valueMatches: {
|
|
* eventType: ['click'],
|
|
* actionType: ['signUp']
|
|
* },
|
|
* nonEmptyFields: ['pageHistory']
|
|
* },
|
|
* fieldConstraints: {
|
|
* parentPageUrl: {
|
|
* scope: 'fullWithoutParams'
|
|
* }
|
|
* }
|
|
* },
|
|
* {
|
|
* filters: {
|
|
* valueMatches: {
|
|
* userType: ['signedIn']
|
|
* }
|
|
* },
|
|
* fieldConstraints: {
|
|
* clientId: {
|
|
* scopeStrategy: 'all'
|
|
* },
|
|
* dsId: { blacklisted: true }
|
|
* }
|
|
* }
|
|
* ]
|
|
* }
|
|
* }
|
|
* } }
|
|
*
|
|
* new Constraints(config).constraintsForEvent({ eventType: 'click', userType: 'signedIn', actionType: 'navigate', ... }) returns:
|
|
* {
|
|
* fieldConstraints: {
|
|
* clientId: {
|
|
* generateValue: true, // from 'any' match
|
|
* tokenSeparator: 'z', // from 'any' match
|
|
* scopeFieldName: 'parentPageUrl', // from 'any' match
|
|
* scopeStrategy: 'all', // from userType=signdIn match
|
|
* expirationPeriod: 86400000 // from 'any' match
|
|
* },
|
|
* dsId: { blacklisted: true }, // from userType=signedIn match
|
|
* parentPageUrl: {
|
|
* scope: 'hostname' // from 'any' match
|
|
* // (the event did not match the rule with eventType=click,
|
|
* // actionType=signUp, nonEmpty pageHistory)
|
|
* }
|
|
* }
|
|
* }
|
|
*/
|
|
LegacyConstraintGenerator.prototype.constraintsForEvent = function constraintsForEvent(eventData, topicConfig, topic) {
|
|
if (!topicConfig) {
|
|
return Promise.resolve(null);
|
|
}
|
|
var self = this;
|
|
|
|
// Use Promise.resolve to wrap the constraintProfiles() here in case of the client delegate the constraintProfile method and returns a non-promise value
|
|
return Promise.resolve(topicConfig.constraintProfile(topic))
|
|
.then(function (constraintProfile) {
|
|
if (!constraintProfile) {
|
|
return null;
|
|
}
|
|
var profilePath = 'constraints.profiles.' + constraintProfile;
|
|
return topicConfig.value(profilePath, topic);
|
|
})
|
|
.then(function (constraintsConfig) {
|
|
var constraints = null;
|
|
|
|
if (constraintsConfig && constraintsConfig.precedenceOrderedRules) {
|
|
constraints = constraintsConfig.precedenceOrderedRules.reduce(function (accumulatedRules, rule) {
|
|
if (self.eventMatchesRule(eventData, rule)) {
|
|
accumulatedRules = self.updateRules(accumulatedRules, rule);
|
|
}
|
|
|
|
return accumulatedRules;
|
|
}, {});
|
|
}
|
|
|
|
return constraints;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {Object} eventData a dictionary of event data
|
|
* @param {Object} matchRule contains information about whether an event matches a rule
|
|
* @return {Boolean}
|
|
* @example
|
|
* var event = { eventType: 'click', userType: 'signedIn', actionType: 'navigate', ... };
|
|
* var matchRule = {
|
|
* filters: {
|
|
* valueMatches: {
|
|
* eventType: ['click']
|
|
* },
|
|
* nonEmptyFields: ['actionType']
|
|
* },
|
|
* fieldConstraints: { ... }
|
|
* };
|
|
* eventMatchesRule(event, matchRule); // => true
|
|
*/
|
|
LegacyConstraintGenerator.prototype.eventMatchesRule = function eventMatchesRule(eventData, matchRule) {
|
|
var returnValue = false;
|
|
|
|
if (eventData && matchRule.filters) {
|
|
if (matchRule.filters === 'any') {
|
|
returnValue = true;
|
|
} else if (reflect.isObject(matchRule.filters)) {
|
|
returnValue =
|
|
this.eventMatchesNonEmptyFields(eventData, matchRule.filters.nonEmptyFields) &&
|
|
this.eventMatchesFieldValues(eventData, matchRule.filters.valueMatches);
|
|
}
|
|
}
|
|
|
|
return returnValue;
|
|
};
|
|
|
|
/**
|
|
* @param {Object} eventData a dictionary of event data
|
|
* @param {Array<string>} nonEmptyFieldNames
|
|
* @return {Boolean}
|
|
*/
|
|
LegacyConstraintGenerator.prototype.eventMatchesNonEmptyFields = function eventMatchesNonEmptyFields(
|
|
eventData,
|
|
nonEmptyFieldNames
|
|
) {
|
|
var returnValue = false;
|
|
|
|
if (eventData) {
|
|
if (!nonEmptyFieldNames || !reflect.isArray(nonEmptyFieldNames)) {
|
|
returnValue = true;
|
|
} else {
|
|
returnValue = nonEmptyFieldNames.every(function (fieldName) {
|
|
return matchers.nonEmpty(fieldName, eventData);
|
|
});
|
|
}
|
|
}
|
|
|
|
return returnValue;
|
|
};
|
|
|
|
/**
|
|
* @param {Object} eventData a dictionary of event data
|
|
* @param {Object} valueMatches a mapping of field names to lists of possible values to match for that field. A matching field value is determined using strict equality. Field values can be of any primitive type.
|
|
* @return {Boolean}
|
|
*/
|
|
LegacyConstraintGenerator.prototype.eventMatchesFieldValues = function eventMatchesFieldValues(
|
|
eventData,
|
|
valueMatches
|
|
) {
|
|
var returnValue = false;
|
|
|
|
if (eventData) {
|
|
if (!valueMatches || !reflect.isObject(valueMatches) || reflect.isEmptyObject(valueMatches)) {
|
|
returnValue = true;
|
|
} else {
|
|
returnValue = Object.keys(valueMatches).every(function (fieldMatchName) {
|
|
var fieldMatchValues = valueMatches[fieldMatchName];
|
|
return matchers.valueMatches(fieldMatchName, eventData, fieldMatchValues);
|
|
});
|
|
}
|
|
}
|
|
|
|
return returnValue;
|
|
};
|
|
|
|
/**
|
|
* @param {Object} currentRules a dictionary of constraint rules
|
|
* @param {Object} newRule a dictionary of rule data to be added to the current rules
|
|
* @return {Object} the updated set of rules
|
|
* Note: the current rules will be modified in place and also returned
|
|
*/
|
|
LegacyConstraintGenerator.prototype.updateRules = function updateRules(currentRules, newRule) {
|
|
return updateFieldRulesets(currentRules, newRule, 'fieldConstraints');
|
|
};
|
|
|
|
/*
|
|
* src/event_actions/denylisted_event_action.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2020 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
var DenylistedEventAction = function DenylistedEventAction() {};
|
|
|
|
/**
|
|
* denylisting the event if the denylisted parameter is true
|
|
* @param {Object} eventData a dictionary of event data
|
|
* @param {Object} originalEventData the original event data
|
|
* @param {Boolean} isDenylisted true if denylisting the entire event
|
|
* @return {Object} return the passed-in eventData if denylisted not equals to false, otherwise, return "null"
|
|
*/
|
|
DenylistedEventAction.prototype.performAction = function performAction(eventData, originalEventData, isDenylisted) {
|
|
return isDenylisted !== true ? eventData : null;
|
|
};
|
|
|
|
/*
|
|
* src/event_actions/denylisted_fields_action.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2021 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
var DenylistedFieldsAction = function DenylistedFieldsAction() {};
|
|
|
|
/**
|
|
* remove the denylisted event fields from the passed-in eventData
|
|
* @param {Object} eventData a dictionary of event data
|
|
* @param {Object} originalEventData the original event data
|
|
* @param {Array} denylistedFields the denylisted fields
|
|
* @return {Object} return a dictionary of event data that excluded the denylisted fields or "null" if all fields are removed.
|
|
*/
|
|
DenylistedFieldsAction.prototype.performAction = function performAction(
|
|
eventData,
|
|
originalEventData,
|
|
denylistedFields
|
|
) {
|
|
if (!eventData || !reflect.isArray(denylistedFields) || reflect.isEmptyArray(denylistedFields)) {
|
|
return eventData;
|
|
}
|
|
eventData = reflect.extend({}, eventData);
|
|
|
|
denylistedFields.forEach(function (denylistedField) {
|
|
delete eventData[denylistedField];
|
|
});
|
|
|
|
return reflect.isEmptyObject(eventData) ? null : eventData;
|
|
};
|
|
|
|
/*
|
|
* src/event_actions/allowlisted_fields_action.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2021 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
var AllowlistedFieldsAction = function AllowlistedFieldsAction() {};
|
|
|
|
/**
|
|
* remove the event fields from the passed-in eventData if it is not in the allowlistedFields
|
|
* @param {Object} eventData a dictionary of event data
|
|
* @param {Object} originalEventData the original event data
|
|
* @param {Array} allowlistedFields the allowlisted fields
|
|
* @return {Object} return a dictionary of event data that only included the allowlisted fields
|
|
*/
|
|
AllowlistedFieldsAction.prototype.performAction = function performAction(
|
|
eventData,
|
|
originalEventData,
|
|
allowlistedFields
|
|
) {
|
|
// Ignoring an empty allowlistedFields to have consistent behavior with Native MetricsKit
|
|
if (!eventData || !reflect.isArray(allowlistedFields) || reflect.isEmptyArray(allowlistedFields)) {
|
|
return eventData;
|
|
}
|
|
var returnedData = {};
|
|
|
|
allowlistedFields.forEach(function (allowlistedField) {
|
|
if (reflect.isDefinedNonNull(eventData[allowlistedField])) {
|
|
returnedData[allowlistedField] = eventData[allowlistedField];
|
|
}
|
|
});
|
|
|
|
return !reflect.isEmptyObject(returnedData) ? returnedData : null;
|
|
};
|
|
|
|
/*
|
|
* src/event_actions/sessionization_fields_action.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2023 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
var MT_SESSIONIZATION_NAMESPACE = 'mtSessionization';
|
|
var STORAGE_KEY_SEPARATOR$1 = '_';
|
|
var ID_TOKEN_SEPARATOR = '-';
|
|
var SESSION_ID_KEY = 'sessionId';
|
|
var SESSION_START_TIME_KEY = 'sessionStartTime';
|
|
var SessionizationFieldsAction = function SessionizationFieldsAction(constraintsInstance) {
|
|
this._constraintsInstance = constraintsInstance;
|
|
};
|
|
|
|
/**
|
|
* attach the session related fields to the event
|
|
* @param {Object} eventData - a dictionary of event data
|
|
* @param {Object} originalEventData - the original event data
|
|
* @param {Object} sessionRules - the session rule object
|
|
* @param {String}(optional) sessionRules.storageKeyPrefix - a prefix to be used when storing ID data in localStorage, default is "mtSessionization"
|
|
* @param {String}(optional) sessionRules.namespace - a string to be used when storing session metadata in localStorage.
|
|
* @param {String}(optional) sessionRules.scopeFieldName - the field name to indicate the scope for the session metadata
|
|
* @param {Number}(optional) sessionRules.idVersion - the version of the session ID
|
|
* @param {String}(optional) sessionRules.tokenSeparator - the separator used to tokenize sections of a finalized, formatted ID string. Default is 'z'
|
|
* @param {Boolean}(optional) sessionRules.sessionStartTime - the flag to indicate whether record "sessionStartTime". Default is false
|
|
* @param {String}(optional) sessionRules.endSessionConditions - the object that contains the conditions to end the existing session
|
|
* @param {String}(optional) sessionRules.endSessionConditions.lifespan - the maximum lifespan for the session (milliseconds)
|
|
* @param {String}(optional) sessionRules.endSessionConditions.idleSpan - the maximum idle span to end the session (milliseconds)
|
|
* @param {String}(optional) sessionRules.endSessionConditions.eventCount - the maximum event count for the session
|
|
* @param {Object}(optional) sessionRules.sessionResetOptions - the reset session options to determine resetting session (deleting the session metadata from storage)
|
|
* @param {Object} sessionRules.sessionResetOptions.filters - the filters-like conditions to execute the session reset
|
|
* @param {Boolean}(optional) sessionRules.sessionResetOptions.newSessionAfterReset - the flag to indicate whether generate a new session after resetting the previous session. Default is false
|
|
* @param {Object}(optional) sessionRules.sessionFields - the mapping of the session field in the event payload
|
|
* @return {Object} return a dictionary of event data that included the session fields
|
|
*/
|
|
SessionizationFieldsAction.prototype.performAction = function performAction(
|
|
eventData,
|
|
originalEventData,
|
|
sessionRules
|
|
) {
|
|
if (!reflect.isDefinedNonNull(eventData) || !reflect.isDefined(sessionRules)) {
|
|
return eventData;
|
|
}
|
|
|
|
if (reflect.isDefinedNonNull(sessionRules.sessionResetOptions)) {
|
|
if (!reflect.isDefinedNonNull(sessionRules.sessionResetOptions.filters)) {
|
|
throw new SyntaxError('sessionizationFields Action: unable to find the required config "filters"');
|
|
}
|
|
var newSessionAfterReset = this._resetSession(eventData, originalEventData, sessionRules);
|
|
if (newSessionAfterReset !== true) {
|
|
return eventData;
|
|
}
|
|
}
|
|
|
|
eventData = reflect.extend({}, eventData);
|
|
|
|
var storageKey = this._storageKey(eventData, sessionRules);
|
|
var environment = this._constraintsInstance.system.environment;
|
|
var sessionMetadata = storage.objectFromStorage(environment.localStorageObject(), storageKey) || {};
|
|
|
|
if (this._shouldCreateNewSession(originalEventData, sessionMetadata, sessionRules)) {
|
|
sessionMetadata.sessionId = this._generateSessionId(sessionRules);
|
|
sessionMetadata.rawFirstEventTimeInSession = originalEventData.eventTime;
|
|
sessionMetadata.firstEventTimeInSession = eventData.eventTime;
|
|
sessionMetadata.eventCount = 0;
|
|
}
|
|
sessionMetadata.rawLastEventTimeInSession = originalEventData.eventTime;
|
|
sessionMetadata.lastEventTimeInSession = eventData.eventTime;
|
|
sessionMetadata.eventCount += 1;
|
|
|
|
storage.saveObjectToStorage(environment.localStorageObject(), storageKey, sessionMetadata);
|
|
|
|
var sessionFieldMap = this._getSessionFieldNames(sessionRules);
|
|
eventData[sessionFieldMap.sessionId] = sessionMetadata.sessionId;
|
|
if (sessionRules.sessionStartTime) {
|
|
eventData[sessionFieldMap.sessionStartTime] = sessionMetadata.firstEventTimeInSession;
|
|
}
|
|
|
|
return eventData;
|
|
};
|
|
|
|
/**
|
|
* @param {Object} eventData a dictionary of event data
|
|
* @param {Object} sessionRules includes information about how to namespace/scope the session data
|
|
* @return {String} the key that session data should be stored under
|
|
* @example
|
|
* (storageKeyPrefix ? storageKeyPrefix : mtSessionization)_<namespace>_(scopeFieldName ? <eventData[scopeFieldName]> : '')
|
|
*/
|
|
SessionizationFieldsAction.prototype._storageKey = function _storageKey(eventData, sessionRules) {
|
|
var scope = this._scope(eventData, sessionRules);
|
|
return this._storageKeyPrefix(sessionRules) + (!reflect.isEmptyString(scope) ? STORAGE_KEY_SEPARATOR$1 + scope : '');
|
|
};
|
|
|
|
/**
|
|
* @param {Object} sessionRules includes information about how to namespace/scope the session data
|
|
* @return {String} a prefix to be used when storing session data in localStorage
|
|
*/
|
|
SessionizationFieldsAction.prototype._storageKeyPrefix = function _storageKeyPrefix(sessionRules) {
|
|
return sessionRules && reflect.isString(sessionRules.storageKeyPrefix) && sessionRules.storageKeyPrefix.length > 0
|
|
? sessionRules.storageKeyPrefix
|
|
: MT_SESSIONIZATION_NAMESPACE;
|
|
};
|
|
|
|
/**
|
|
* @param {Object} eventData a dictionary of event data
|
|
* @param {Object} sessionRules includes information about how to namespace/scope the session data
|
|
* @return {String} the namespace/scope for this set of event data and rules
|
|
*/
|
|
SessionizationFieldsAction.prototype._scope = function _scope(eventData, sessionRules) {
|
|
var sessionScope = '';
|
|
|
|
if (reflect.isDefined(sessionRules)) {
|
|
if (reflect.isString(sessionRules.namespace)) {
|
|
sessionScope += sessionRules.namespace;
|
|
}
|
|
|
|
if (
|
|
reflect.isString(sessionRules.scopeFieldName) &&
|
|
reflect.isDefinedNonNull(eventData[sessionRules.scopeFieldName])
|
|
) {
|
|
sessionScope += STORAGE_KEY_SEPARATOR$1;
|
|
sessionScope += eventData[sessionRules.scopeFieldName].toString();
|
|
}
|
|
}
|
|
|
|
return sessionScope;
|
|
};
|
|
|
|
/**
|
|
* generate session ID based on the provided session rules
|
|
* @param {Object} sessionRules
|
|
* @returns {String} the generated session ID
|
|
* @private
|
|
*/
|
|
SessionizationFieldsAction.prototype._generateSessionId = function _generateSessionId(sessionRules) {
|
|
return generateId({
|
|
idVersion: 1,
|
|
time: Date.now(),
|
|
idTokenSeparator: ID_TOKEN_SEPARATOR,
|
|
generatedIdSeparator: sessionRules.tokenSeparator
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Decide whether create a new session based on the current session metadata and the session rules
|
|
* @param sessionMetadata
|
|
* @param sessionRules
|
|
* @returns {Boolean}
|
|
* @private
|
|
*/
|
|
SessionizationFieldsAction.prototype._shouldCreateNewSession = function _shouldCreateNewSession(
|
|
event,
|
|
sessionMetadata,
|
|
sessionRules
|
|
) {
|
|
var shouldCreateNewSession = false;
|
|
shouldCreateNewSession |= !reflect.isDefinedNonNull(sessionMetadata.sessionId);
|
|
|
|
if (reflect.isDefinedNonNull(sessionRules.endSessionConditions)) {
|
|
if (reflect.isDefinedNonNull(sessionRules.endSessionConditions.lifespan)) {
|
|
shouldCreateNewSession |=
|
|
event.eventTime >=
|
|
sessionMetadata.rawFirstEventTimeInSession + sessionRules.endSessionConditions.lifespan;
|
|
}
|
|
|
|
if (reflect.isDefinedNonNull(sessionRules.endSessionConditions.idleSpan)) {
|
|
shouldCreateNewSession |=
|
|
event.eventTime >=
|
|
sessionMetadata.rawLastEventTimeInSession + sessionRules.endSessionConditions.idleSpan;
|
|
}
|
|
|
|
if (reflect.isDefinedNonNull(sessionRules.endSessionConditions.eventCount)) {
|
|
shouldCreateNewSession |= sessionMetadata.eventCount >= sessionRules.endSessionConditions.eventCount;
|
|
}
|
|
}
|
|
|
|
return shouldCreateNewSession;
|
|
};
|
|
|
|
/**
|
|
* Reset the existing session based on the session rules
|
|
* @param {Object} eventData
|
|
* @param {Object} originalEventData
|
|
* @param {Object} sessionRules
|
|
* @returns {Boolean} The flag indicates whether to process the rest of logic
|
|
*/
|
|
SessionizationFieldsAction.prototype._resetSession = function _resetSession(
|
|
eventData,
|
|
originalEventData,
|
|
sessionRules
|
|
) {
|
|
var sessionResetOptions = sessionRules.sessionResetOptions;
|
|
var constraintsGenerator = this._constraintsInstance._constraintGenerator;
|
|
if (
|
|
reflect.isDefinedNonNull(constraintsGenerator) &&
|
|
reflect.isDefinedNonNull(constraintsGenerator.eventMatchesTreatment) &&
|
|
constraintsGenerator.eventMatchesTreatment(originalEventData, sessionResetOptions)
|
|
) {
|
|
var storageKey = this._storageKey(eventData, sessionRules);
|
|
var environment = this._constraintsInstance.system.environment;
|
|
storage.saveObjectToStorage(environment.localStorageObject(), storageKey, undefined);
|
|
|
|
return sessionResetOptions.newSessionAfterReset;
|
|
}
|
|
// Always continue the sessionization logic if the resetting is not applied.
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Return a map from session field names to their associated event field names
|
|
* @param {Object} sessionRoles
|
|
* @returns {Object} A map between the session field names and their associated event field names
|
|
*/
|
|
SessionizationFieldsAction.prototype._getSessionFieldNames = function _getSessionFieldNames(sessionRoles) {
|
|
var sessionFieldNames = {
|
|
sessionId: SESSION_ID_KEY,
|
|
sessionStartTime: SESSION_START_TIME_KEY
|
|
};
|
|
if (reflect.isDefinedNonNull(sessionRoles.sessionFields)) {
|
|
if (reflect.isDefinedNonNullNonEmpty(sessionRoles.sessionFields[SESSION_ID_KEY])) {
|
|
sessionFieldNames.sessionId = sessionRoles.sessionFields[SESSION_ID_KEY];
|
|
}
|
|
if (reflect.isDefinedNonNullNonEmpty(sessionRoles.sessionFields[SESSION_START_TIME_KEY])) {
|
|
sessionFieldNames.sessionStartTime = sessionRoles.sessionFields[SESSION_START_TIME_KEY];
|
|
}
|
|
}
|
|
|
|
return sessionFieldNames;
|
|
};
|
|
|
|
/*
|
|
* src/event_actions/index.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2020 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
var ACTIONS = {
|
|
blacklistedEventAction: 'blacklisted', // DEPRECATED, use denylistedEventAction instead
|
|
denylistedEventAction: 'denylisted',
|
|
blacklistedFieldsAction: 'blacklistedFields', // DEPRECATED, use denylistedFieldsAction instead
|
|
denylistedFieldsAction: 'denylistedFields',
|
|
whitelistedFieldsAction: 'whitelistedFields', // DEPRECATED, use allowlistedFieldsAction instead
|
|
allowlistedFieldsAction: 'allowlistedFields',
|
|
sessionizationFieldsAction: 'sessionizationFields'
|
|
};
|
|
|
|
var EventActions = function EventActions(constraintsInstance) {
|
|
var denylistedEventAction = new DenylistedEventAction();
|
|
var denylistedFieldsAction = new DenylistedFieldsAction();
|
|
var allowlistedFieldsAction = new AllowlistedFieldsAction();
|
|
var sessionizationFieldsAction = new SessionizationFieldsAction(constraintsInstance);
|
|
|
|
// @private
|
|
this._actions = {};
|
|
this._actions[ACTIONS.blacklistedEventAction] = denylistedEventAction; // mapping to equivalent but Inclusive Termed method
|
|
this._actions[ACTIONS.denylistedEventAction] = denylistedEventAction;
|
|
this._actions[ACTIONS.blacklistedFieldsAction] = denylistedFieldsAction; // mapping to equivalent but Inclusive Termed method
|
|
this._actions[ACTIONS.denylistedFieldsAction] = denylistedFieldsAction;
|
|
this._actions[ACTIONS.whitelistedFieldsAction] = allowlistedFieldsAction; // mapping to equivalent but Inclusive Termed method
|
|
this._actions[ACTIONS.allowlistedFieldsAction] = allowlistedFieldsAction;
|
|
this._actions[ACTIONS.sessionizationFieldsAction] = sessionizationFieldsAction;
|
|
};
|
|
|
|
EventActions.prototype.getAction = function getAction(actionName) {
|
|
return this._actions[actionName];
|
|
};
|
|
|
|
/*
|
|
* src/field_actions/number_action.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2020 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
var START_KEY = 'start';
|
|
var VALUE_KEY = 'value';
|
|
|
|
/**
|
|
* Returns the index at which you should insert the object in order to maintain a sorted array
|
|
* @param {Array} array - The sorted array to inspect, must be a list without undefined/null values.
|
|
* @param {Number} value - The value to evaluate
|
|
* @returns {number} Returns the index at which `value` should be inserted, -1 if the value is less than the first item, the array or value is undefined/null
|
|
*/
|
|
var searchInsertionIndexOf = function searchInsertionIndexOf(array, value) {
|
|
var NOT_FOUND_OUTPUT = -1;
|
|
var index = NOT_FOUND_OUTPUT;
|
|
|
|
if (
|
|
!reflect.isDefinedNonNull(value) ||
|
|
array.length === 0 ||
|
|
// classify the numbers less than the lowest bucket
|
|
// -> array = [10, 20, 30], value = 9
|
|
// <- -1
|
|
(reflect.isDefinedNonNull(array[0]) && value < array[0][START_KEY])
|
|
) {
|
|
return NOT_FOUND_OUTPUT;
|
|
}
|
|
|
|
// Using a linear search instead of binary search because the array won't be large and less error-prone
|
|
if (array[array.length - 1][START_KEY] < value) {
|
|
index = array.length - 1;
|
|
} else {
|
|
for (var i = 0; i < array.length; i++) {
|
|
var start = array[i][START_KEY];
|
|
if (start === value) {
|
|
index = i;
|
|
break;
|
|
} else if (start > value) {
|
|
index = i - 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return index;
|
|
};
|
|
|
|
var NumberAction = function NumberAction() {
|
|
Base$1.apply(this, arguments);
|
|
};
|
|
|
|
NumberAction.prototype = Object.create(Base$1.prototype);
|
|
NumberAction.prototype.constructor = NumberAction;
|
|
|
|
/**
|
|
* @param {Number} aNumber - a number being constrained
|
|
* @param {Object} fieldRules - includes information about how to constrain the field
|
|
* @param {Number} fieldRules.precision - must be a positive integer
|
|
* @return {Number} the constrained number
|
|
*/
|
|
NumberAction.prototype.constrainedValue = function constrainedValue(aNumber, fieldRules) {
|
|
var precision = fieldRules ? fieldRules.precision : 0;
|
|
var buckets = fieldRules ? fieldRules.buckets : null;
|
|
|
|
if (reflect.isDefinedNonNullNonEmpty(buckets)) {
|
|
buckets = buckets.slice().sort(function (a, b) {
|
|
return a[START_KEY] - b[START_KEY];
|
|
});
|
|
|
|
var bucketIndex = searchInsertionIndexOf(buckets, aNumber);
|
|
var bucket = buckets[bucketIndex];
|
|
|
|
if (reflect.isDefinedNonNull(bucket)) {
|
|
aNumber = bucket[VALUE_KEY];
|
|
}
|
|
} else if (reflect.isNumber(aNumber) && reflect.isNumber(precision) && precision > 0) {
|
|
aNumber = Math.floor(aNumber / precision) * precision;
|
|
}
|
|
|
|
return aNumber;
|
|
};
|
|
|
|
/*
|
|
* src/utils/serial_number_generator.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2020 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
var DEFAULT_NAMESPACE = 'mt_serial_number';
|
|
var EXPIRATION_STORAGE_KEY = 'exp';
|
|
var SERIAL_NUMBER_STORAGE_KEY = 'sn';
|
|
|
|
/**
|
|
*
|
|
* @param {Object} options - An object that contains parameters for generating serial numbers
|
|
* @param {String} options.namespace (optional) - A key that is used to store the serial numbers in the Storage. Default to "mt_serial_number"
|
|
* @param {Number} options.initialSerialNumber (optional) - An initialization serial number. Default to 0
|
|
* @param {Number} options.nextRotationTime (optional) - A timestamp that indicates when should reset the serial number. Default to Number.POSITIVE_INFINITY(never rotate)
|
|
* @param {Number} options.rotationPeriod (optional) - A millisecond to indicate how long the serial number could be alive. Default to Number.POSITIVE_INFINITY(never rotate)
|
|
* @constructor
|
|
*/
|
|
var SerialNumberGenerator = function SerialNumberGenerator(options) {
|
|
options = options || {};
|
|
// @private
|
|
this._nextRotationTime = options.nextRotationTime || Number.POSITIVE_INFINITY;
|
|
// @private
|
|
this._storageKey = options.namespace || DEFAULT_NAMESPACE;
|
|
// @private
|
|
this._initialSerialNumber = options.initialSerialNumber || 0;
|
|
// @private
|
|
this._rotationPeriod = options.rotationPeriod || Number.POSITIVE_INFINITY;
|
|
};
|
|
|
|
SerialNumberGenerator.prototype.setDelegate = function setDelegate(delegate) {
|
|
reflect.attachDelegate(this, delegate);
|
|
};
|
|
|
|
SerialNumberGenerator.prototype.localStorageObject = function localStorageObject() {
|
|
return storage.localStorageObject();
|
|
};
|
|
|
|
/**
|
|
* Return the increased serial number
|
|
* @param {Number} increment (optional) - The amount to increment. Defaults to 1
|
|
* @returns {Number} the increased serial number
|
|
*/
|
|
SerialNumberGenerator.prototype.getNextSerialNumber = function getNextSerialNumber(increment) {
|
|
var storageKey = this._storageKey;
|
|
var serialNumberData = this._getCurrentSerialNumberData(storageKey);
|
|
|
|
var serialNum = serialNumberData[SERIAL_NUMBER_STORAGE_KEY];
|
|
increment = reflect.isNumber(increment) ? increment : 1;
|
|
serialNum = parseInt(serialNum, 10);
|
|
|
|
if (isNaN(serialNum)) {
|
|
// Reset the serial number to the initialized one, to ensure the logic won't break if the sequence number is an invalid number.
|
|
serialNum = this._initialSerialNumber;
|
|
}
|
|
serialNum = this._increaseSerialNumber(serialNum, increment);
|
|
// Store the increased serial number to storage
|
|
serialNumberData[SERIAL_NUMBER_STORAGE_KEY] = serialNum;
|
|
storage.saveObjectToStorage(this.localStorageObject(), this._storageKey, serialNumberData);
|
|
|
|
return serialNum;
|
|
};
|
|
|
|
/**
|
|
* Reset the serial number
|
|
*/
|
|
SerialNumberGenerator.prototype.resetSerialNumber = function resetSerialNumber() {
|
|
var serialNumberData = storage.objectFromStorage(this.localStorageObject(), this._storageKey);
|
|
|
|
if (reflect.isDefinedNonNull(serialNumberData)) {
|
|
this._resetSerialNumber(serialNumberData[EXPIRATION_STORAGE_KEY]);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Delegable method to return the time for calculating rotation
|
|
* @returns {number}
|
|
*/
|
|
SerialNumberGenerator.prototype.getTime = function getTime() {
|
|
return Date.now();
|
|
};
|
|
|
|
/**
|
|
* Increasing the giving serial number by plus one
|
|
* @param {Number} serialNum
|
|
* @returns {number}
|
|
* @private
|
|
*/
|
|
SerialNumberGenerator.prototype._increaseSerialNumber = function _increaseSerialNumber(serialNum, increment) {
|
|
return serialNum + increment;
|
|
};
|
|
|
|
/**
|
|
* Rotate and return the serial number data
|
|
* @param {String} storageKey
|
|
* @returns {Object} rotated serial number data
|
|
*/
|
|
SerialNumberGenerator.prototype._getCurrentSerialNumberData = function _getCurrentSerialNumberData(storageKey) {
|
|
var serialNumberData = storage.objectFromStorage(this.localStorageObject(), storageKey);
|
|
var rotationTime;
|
|
var nextRotationTime;
|
|
if (serialNumberData) {
|
|
rotationTime = serialNumberData[EXPIRATION_STORAGE_KEY];
|
|
rotationTime = parseInt(rotationTime, 10);
|
|
serialNumberData[EXPIRATION_STORAGE_KEY] = rotationTime = isNaN(rotationTime)
|
|
? this._nextRotationTime
|
|
: rotationTime;
|
|
} else {
|
|
// use the "nextRotationTime - rotationPeriod" as the rotationTime if the serialNumberData is not existing in the storage, to check if need to reset serial number
|
|
rotationTime = this._nextRotationTime - this._rotationPeriod;
|
|
}
|
|
|
|
// Reset the serial number data if it has expired or never initialized
|
|
// Checking "!serialNumberData" in here to cover the case of when both of this._nextRotationTime and this._rotationPeriod are not provided, "this._nextRotationTime(Infinite) - this._rotationPeriod(Infinite) = NaN" which is always less than "this.getTime()"
|
|
// Use while loop here to catch up to the latest rotation time.
|
|
while (!serialNumberData || this.getTime() >= rotationTime) {
|
|
rotationTime = nextRotationTime = rotationTime + this._rotationPeriod;
|
|
serialNumberData = this._resetSerialNumber(nextRotationTime);
|
|
}
|
|
return serialNumberData;
|
|
};
|
|
|
|
/**
|
|
* Reset the serial number and expiration
|
|
* @param {Number} nextRotationTime - A timestamp that indicates when should reset the serial number
|
|
* @returns reset serialNumberData
|
|
* {
|
|
* exp: nextRotationTime,
|
|
* sn: serialNumber
|
|
* }
|
|
*/
|
|
SerialNumberGenerator.prototype._resetSerialNumber = function _resetSerialNumber(nextRotationTime) {
|
|
var serialNumberData = {};
|
|
serialNumberData[EXPIRATION_STORAGE_KEY] = nextRotationTime;
|
|
serialNumberData[SERIAL_NUMBER_STORAGE_KEY] = this._initialSerialNumber;
|
|
storage.saveObjectToStorage(this.localStorageObject(), this._storageKey, serialNumberData);
|
|
return serialNumberData;
|
|
};
|
|
|
|
/*
|
|
* src/field_actions/time_action.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2020 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
var STORAGE_KEY_SEPARATOR$2 = '_';
|
|
var STORAGE_PREFIX_DEFAULT = 'mtTimestamp';
|
|
|
|
var TimeAction = function TimeAction() {
|
|
Base$1.apply(this, arguments);
|
|
// @private
|
|
this._storage = this._constraintsInstance.system.environment.localStorageObject();
|
|
// @private
|
|
/*
|
|
* Store the end time of the giving time precision base on namespace + time fields
|
|
*/
|
|
this._precisionEndTimeCache = {};
|
|
// @private
|
|
this._serialNumberGenerator = null;
|
|
};
|
|
|
|
TimeAction.prototype = Object.create(Base$1.prototype);
|
|
TimeAction.prototype.constructor = TimeAction;
|
|
|
|
/**
|
|
* @param {Number} time - a timestamp being constrained
|
|
* @param {Object} fieldRules - includes information about how to constrain the field
|
|
* @param {Object} fieldRules.precision - The time must be a positive integer
|
|
* @param {String} fieldRules.storageKeyPrefix - a prefix to be used when storing timestamp de-res related data, default is "mt-timestamp"
|
|
* @param {String} fieldRules.namespace - a namespace for the timestamp de-res related data, default is empty.
|
|
* @param {Object} eventData - a dictionary of event data, which should include a pre-existing (unconstrained) field
|
|
* @param {String} fieldName - name of the field being modified in eventData
|
|
* @return {Number} the constrained time or the original value if time is not defined or fieldRules is unavailable
|
|
*/
|
|
TimeAction.prototype.constrainedValue = function constrainedValue(time, fieldRules, eventData, fieldName) {
|
|
var returnTimestamp = time;
|
|
if (
|
|
reflect.isNumber(time) &&
|
|
reflect.isObject(fieldRules) &&
|
|
reflect.isNumber(fieldRules.precision) &&
|
|
fieldRules.precision > 0
|
|
) {
|
|
var precisionStartTime = this._computePrecisionStartTime(time, fieldRules);
|
|
this._serialNumberGenerator = new SerialNumberGenerator({
|
|
namespace: this._persistentStorageKey(fieldRules, fieldName),
|
|
nextRotationTime: precisionStartTime + fieldRules.precision,
|
|
rotationPeriod: fieldRules.precision
|
|
});
|
|
this._serialNumberGenerator.setDelegate(this._constraintsInstance.system.environment);
|
|
this._serialNumberGenerator.setDelegate({
|
|
getTime: function () {
|
|
return time;
|
|
}
|
|
});
|
|
var serialNumber = this._serialNumberGenerator.getNextSerialNumber();
|
|
returnTimestamp = this._computeTimestamp(precisionStartTime, serialNumber);
|
|
this._serialNumberGenerator = null; // Release the serial number generator.
|
|
}
|
|
|
|
return returnTimestamp;
|
|
};
|
|
|
|
TimeAction.prototype._computeTimestamp = function _computeTimestamp(precisionStartTime, sequenceNum) {
|
|
return precisionStartTime + sequenceNum;
|
|
};
|
|
|
|
TimeAction.prototype._persistentStorageKey = function _persistentStorageKey(fieldRules, fieldName) {
|
|
var namespaceSegment = fieldRules.namespace ? STORAGE_KEY_SEPARATOR$2 + fieldRules.namespace : '';
|
|
return (
|
|
(fieldRules.storageKeyPrefix || STORAGE_PREFIX_DEFAULT) + namespaceSegment + STORAGE_KEY_SEPARATOR$2 + fieldName
|
|
);
|
|
};
|
|
|
|
TimeAction.prototype._computePrecisionStartTime = function _computePrecisionStartTime(time, fieldRules) {
|
|
var precision = fieldRules.precision;
|
|
return Math.floor(time / precision) * precision;
|
|
};
|
|
|
|
/*
|
|
* src/field_actions/hash_action.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2022 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
var STORAGE_KEY_SEPARATOR$3 = '_';
|
|
var STORAGE_PREFIX_DEFAULT$1 = 'mtHash';
|
|
var STORAGE_SALT_KEY = 'salt';
|
|
var SALT_CHAR_LENGTH = 10;
|
|
|
|
var HashAction = function HashAction() {
|
|
Base$1.apply(this, arguments);
|
|
};
|
|
HashAction.prototype = Object.create(Base$1.prototype);
|
|
HashAction.prototype.constructor = HashAction;
|
|
|
|
/**
|
|
* Build the storage key for salt data
|
|
* key format: <prefix|mtHash>_<namespace?>_salt_<fieldName>
|
|
* @param fieldRules
|
|
* @param fieldName
|
|
* @returns {string}
|
|
*/
|
|
function buildSaltStorageKey(fieldRules, fieldName) {
|
|
var namespaceSegment = fieldRules.namespace ? STORAGE_KEY_SEPARATOR$3 + fieldRules.namespace : '';
|
|
return (
|
|
(fieldRules.storageKeyPrefix || STORAGE_PREFIX_DEFAULT$1) +
|
|
namespaceSegment +
|
|
STORAGE_KEY_SEPARATOR$3 +
|
|
STORAGE_SALT_KEY +
|
|
STORAGE_KEY_SEPARATOR$3 +
|
|
fieldName
|
|
);
|
|
}
|
|
|
|
function generateSalt() {
|
|
var salt = '';
|
|
while (salt.length < SALT_CHAR_LENGTH) {
|
|
salt += string.randomHexCharacter();
|
|
}
|
|
return salt;
|
|
}
|
|
|
|
// The hash logic is borrowed from String.hashcode() of Java
|
|
function hashCode(value, salt) {
|
|
return [value, salt]
|
|
.map(function (segment) {
|
|
var hash = 0;
|
|
// undefined, null and '' will return 0 as the hash code.
|
|
if (reflect.isDefinedNonNullNonEmpty(segment)) {
|
|
for (var i = 0; i < segment.length; i++) {
|
|
var charCode = segment.charCodeAt(i);
|
|
hash = (hash << 5) - hash + charCode; // "(hash << 5) - hash" is similar to "hash * 31" but faster.
|
|
}
|
|
}
|
|
var hashedValue = Math.abs(hash);
|
|
hashedValue = parseInt(hashedValue, 16);
|
|
return string.convertNumberToBaseAlphabet(hashedValue, string.base62Alphabet);
|
|
})
|
|
.join('');
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {String} value The value that will be hashed
|
|
* @param {Object} fieldRules - includes information about how to constrain the field
|
|
* @param {String} fieldRules.storageKeyPrefix - a prefix to be used when storing hash related data, default is "mtHash"
|
|
* @param {String} fieldRules.namespace - a namespace for storing the hash data, default is empty.
|
|
* @param {Number} fieldRules.saltLifespan - a lifespan (milliseconds) of the salt
|
|
* @param {Object} fieldRules.platformBasedSalt - a config section includes the configures for loading salt from platform API
|
|
* @param {String} fieldRules.platformBasedSalt.saltNamespace - a namespace that stores the salt configuration in the "metricsIdentifier" section of the bag
|
|
* @param {String} fieldRules.platformBasedSalt.crossDeviceSync
|
|
* @param {Object} eventData - a dictionary of event data, which should include a pre-existing (unconstrained) field
|
|
* @param {String} fieldName - name of the field being modified in eventData
|
|
* @return {String | Promise<string>} The hashed value on the top of provided value with the stored salt (rotated for every <fieldRules.saltLifespan> milliseconds).
|
|
*/
|
|
HashAction.prototype.constrainedValue = function constrainedValue(value, fieldRules, _eventData, fieldName) {
|
|
if (reflect.isDefinedNonNullNonEmpty(value)) {
|
|
return this._loadPlatformBasedSalt(fieldRules, fieldName).then(function (salt) {
|
|
return hashCode(value, salt);
|
|
});
|
|
}
|
|
return value;
|
|
};
|
|
|
|
/**
|
|
* @param {Number} timestamp a timestamp in ms since epoch
|
|
* @return {Boolean} return false if timestamp does not exist
|
|
* @overridable
|
|
*/
|
|
HashAction.prototype.timeExpired = function timeExpired(timestamp) {
|
|
return timestamp ? timestamp <= Date.now() : false;
|
|
};
|
|
|
|
/**
|
|
* @param {Number} (optional) lifespan the amount of time, in milliseconds, that an ID should be valid for
|
|
* @return {Number} a timestamp in ms since epoch, or null if no lifespan was provided
|
|
* @overridable
|
|
*/
|
|
HashAction.prototype.expirationTime = function expirationTime(lifespan) {
|
|
return lifespan ? Date.now() + lifespan : null;
|
|
};
|
|
|
|
HashAction.prototype._loadPlatformBasedSalt = function _loadPlatformBasedSalt(fieldRules, fieldName) {
|
|
var saltPromise = null;
|
|
var self = this;
|
|
var platformBasedSaltConfig = fieldRules.platformBasedSalt;
|
|
if (reflect.isDefinedNonNull(platformBasedSaltConfig)) {
|
|
saltPromise = this._constraintsInstance.system.environment.platformIdentifier(
|
|
platformBasedSaltConfig.saltNamespace,
|
|
'userid',
|
|
platformBasedSaltConfig.crossDeviceSync || true
|
|
);
|
|
if (reflect.isDefinedNonNull(saltPromise)) {
|
|
saltPromise = saltPromise.then(function (salt) {
|
|
if (!reflect.isDefinedNonNull(salt)) {
|
|
self._constraintsInstance.system.logger.warn(
|
|
'Hash: platform returned an empty salt. Will use default salt generator to generate the salt.'
|
|
);
|
|
salt = self._getSalt(fieldRules, fieldName);
|
|
}
|
|
return salt;
|
|
});
|
|
} else {
|
|
saltPromise = Promise.resolve(this._getSalt(fieldRules, fieldName));
|
|
}
|
|
} else {
|
|
saltPromise = Promise.resolve(this._getSalt(fieldRules, fieldName));
|
|
}
|
|
|
|
return saltPromise;
|
|
};
|
|
|
|
// This method retrieves the salt from storage, otherwise generates a new salt if it doesn't exist or has expired
|
|
HashAction.prototype._getSalt = function _getSalt(fieldRules, fieldName) {
|
|
var saltMetadata = this._retrieveSaltFromStorage(fieldRules, fieldName);
|
|
var saltLifespan = fieldRules.saltLifespan;
|
|
if (!reflect.isDefinedNonNull(saltMetadata) || this.timeExpired(saltMetadata.expirationTime)) {
|
|
var localStorage = this._constraintsInstance.system.environment.localStorageObject();
|
|
var salt = generateSalt();
|
|
saltMetadata = {
|
|
salt: salt,
|
|
expirationTime: this.expirationTime(saltLifespan)
|
|
};
|
|
storage.saveObjectToStorage(localStorage, buildSaltStorageKey(fieldRules, fieldName), saltMetadata);
|
|
}
|
|
|
|
return saltMetadata.salt;
|
|
};
|
|
|
|
HashAction.prototype._retrieveSaltFromStorage = function _retrieveSaltFromStorage(fieldRules, fieldName) {
|
|
var localStorage = this._constraintsInstance.system.environment.localStorageObject();
|
|
var saltMetadata = storage.objectFromStorage(localStorage, buildSaltStorageKey(fieldRules, fieldName));
|
|
return saltMetadata;
|
|
};
|
|
|
|
/*
|
|
* src/field_actions/index.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2020 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
var ACTIONS$1 = {
|
|
ID: 'idGenerator',
|
|
NUMBER: 'numberDeres',
|
|
TIME: 'timeDeres',
|
|
URL: 'urlDeres',
|
|
HASH: 'hash'
|
|
};
|
|
|
|
var FieldActions = function DeresHandlers(constraintsInstance) {
|
|
this.actions = {};
|
|
this.actions[ACTIONS$1.ID] = new IdAction(constraintsInstance);
|
|
this.actions[ACTIONS$1.NUMBER] = new NumberAction(constraintsInstance);
|
|
this.actions[ACTIONS$1.TIME] = new TimeAction(constraintsInstance);
|
|
this.actions[ACTIONS$1.URL] = new UrlAction(constraintsInstance);
|
|
this.actions[ACTIONS$1.HASH] = new HashAction(constraintsInstance);
|
|
};
|
|
|
|
FieldActions.prototype.getAction = function getAction(actionName) {
|
|
return this.actions[actionName];
|
|
};
|
|
|
|
/*
|
|
* src/treatment/action_treatment.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2020 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
var ActionTreatment = function ActionTreatment(constraintInstance) {
|
|
// @private
|
|
this._eventActions = new EventActions(constraintInstance);
|
|
|
|
// @private
|
|
this._fieldActions = new FieldActions(constraintInstance);
|
|
};
|
|
|
|
/**
|
|
* @param {Object} eventData a dictionary of event data
|
|
* @param {Object} eventConstraints a set of constraints to apply to this event
|
|
* @return {Object | Promise} constraints the event data modified according to the appropriate constraints or "null" if the event is blacklisted
|
|
* Note:
|
|
* 1. create a new dictionary if the event data is constrained/modified
|
|
* 2. return the original eventData if the constraints parameter is null or an empty dictionary
|
|
* @example
|
|
* var eventData = {
|
|
* eventType: 'click',
|
|
* pageType: 'TopCharts',
|
|
* parentPageUrl: 'https://itunes.apple.com/music/topcharts/12345',
|
|
* // etc.
|
|
* };
|
|
* var eventConstraints = {
|
|
* eventActions: { blacklistedFields: ['cookies', 'pageDetails'] },
|
|
* fieldActions: {
|
|
* parentPageUrl: {
|
|
* treatmentType: 'urlDeres',
|
|
* scope: 'hostname'
|
|
* }
|
|
* }
|
|
* }
|
|
* constraints.eventFields.applyEventConstraints(eventData, eventConstraints) =>
|
|
* {
|
|
* eventType: click,
|
|
* pageType: 'TopCharts',
|
|
* parentPageUrl: 'itunes.apple.com', // truncated to hostname only
|
|
* etc... // all other fields remain the same, except "cookies", "pageDetails"
|
|
* }
|
|
*/
|
|
ActionTreatment.prototype.applyConstraints = function applyConstraints(eventData, constraints) {
|
|
var returnEventData = eventData; // Set the original eventData to the returning variable to return the original eventData if neither event actions nor field actions were applied.
|
|
|
|
if (constraints && !reflect.isEmptyObject(constraints)) {
|
|
var promiseTasks = [];
|
|
var self = this;
|
|
|
|
if (constraints.fieldActions && !reflect.isEmptyObject(constraints.fieldActions)) {
|
|
var isAnyFieldChanged = false;
|
|
var eventDataCopy = returnEventData;
|
|
eventDataCopy = Object.keys(constraints.fieldActions).reduce(function (accumulatedFields, fieldName) {
|
|
var fieldRules = constraints.fieldActions[fieldName];
|
|
if (fieldRules) {
|
|
var denylisted = fieldRules.denylisted || fieldRules.blacklisted;
|
|
var fieldAction = fieldRules.treatmentType;
|
|
var fieldActionHandler = self._fieldActions.getAction(fieldAction);
|
|
|
|
accumulatedFields = lookForKeyPath(
|
|
accumulatedFields,
|
|
fieldName,
|
|
false,
|
|
function (value, key, keyPath, object) {
|
|
if (denylisted) {
|
|
delete object[key];
|
|
isAnyFieldChanged = true;
|
|
} else if (fieldRules.hasOwnProperty(FIELD_RULES.OVERRIDE_FIELD_VALUE)) {
|
|
object[key] = fieldRules[FIELD_RULES.OVERRIDE_FIELD_VALUE];
|
|
isAnyFieldChanged = true;
|
|
} else if (fieldActionHandler) {
|
|
var returnedValue = fieldActionHandler.performAction(
|
|
value,
|
|
fieldName,
|
|
returnEventData,
|
|
fieldRules
|
|
);
|
|
object[key] = returnedValue;
|
|
if (returnedValue instanceof Promise) {
|
|
promiseTasks.push(
|
|
returnedValue.then(function (processedValue) {
|
|
lookForKeyPath(
|
|
eventDataCopy,
|
|
fieldName,
|
|
true,
|
|
function (_value, targetKey, targetKeyPath, targetObject) {
|
|
if (targetKeyPath === keyPath) {
|
|
targetObject[targetKey] = processedValue;
|
|
}
|
|
}
|
|
);
|
|
})
|
|
);
|
|
}
|
|
isAnyFieldChanged = true;
|
|
}
|
|
}
|
|
);
|
|
}
|
|
return accumulatedFields;
|
|
}, eventDataCopy);
|
|
|
|
// If any field has been constrained, we create a new object to contain the merged fields instead of merging the changes to the original eventData.
|
|
// eventDataCopy has been re-built by "lookForKeyPath" above.
|
|
if (isAnyFieldChanged) {
|
|
if (promiseTasks.length > 0) {
|
|
returnEventData = Promise.all(promiseTasks).then(function () {
|
|
return eventDataCopy;
|
|
});
|
|
} else {
|
|
returnEventData = eventDataCopy;
|
|
}
|
|
}
|
|
}
|
|
|
|
// perform event actions after the field actions to ensure removing denied fields or keeping allowed fields for those generated fields(e.g. IdGenerator).
|
|
if (constraints.eventActions && !reflect.isEmptyObject(constraints.eventActions)) {
|
|
var eventActionNames = Object.keys(constraints.eventActions);
|
|
var processEventActions = function (processingEventData) {
|
|
eventActionNames.forEach(function (eventAction) {
|
|
var eventActionHandler = self._eventActions.getAction(eventAction);
|
|
if (eventActionHandler) {
|
|
var actionRules = constraints.eventActions[eventAction];
|
|
processingEventData = eventActionHandler.performAction(
|
|
processingEventData,
|
|
eventData,
|
|
actionRules
|
|
);
|
|
}
|
|
});
|
|
return processingEventData;
|
|
};
|
|
|
|
if (returnEventData instanceof Promise) {
|
|
returnEventData = Promise.resolve(returnEventData).then(function (processedEventData) {
|
|
return processEventActions(processedEventData);
|
|
});
|
|
} else {
|
|
returnEventData = processEventActions(returnEventData);
|
|
}
|
|
}
|
|
}
|
|
|
|
return returnEventData;
|
|
};
|
|
|
|
/*
|
|
* src/constraint_generator/treatment_generator.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2017-2018 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
var TREATMENT_FILTERS_FIELD = 'filters';
|
|
var TREATMENT_FILTERS_ALL = 'any';
|
|
var TREATMENT_EVENT_ACTIONS = 'eventActions';
|
|
var TREATMENT_FIELD_ACTIONS = 'fieldActions';
|
|
|
|
function _updateTreatment(accumulatedTreatment, treatment) {
|
|
var currentTreatment = accumulatedTreatment || {};
|
|
// update event actions
|
|
_updateEventActions(currentTreatment, treatment);
|
|
// update field actions
|
|
_updateFieldActions(currentTreatment, treatment);
|
|
|
|
return currentTreatment;
|
|
}
|
|
|
|
function _updateEventActions(targetTreatment, sourceTreatment) {
|
|
if (!targetTreatment[TREATMENT_EVENT_ACTIONS]) {
|
|
targetTreatment[TREATMENT_EVENT_ACTIONS] = {};
|
|
}
|
|
var currentTreatmentEventActions = targetTreatment[TREATMENT_EVENT_ACTIONS];
|
|
var treatmentEventActions = sourceTreatment[TREATMENT_EVENT_ACTIONS];
|
|
|
|
if (treatmentEventActions) {
|
|
Object.keys(treatmentEventActions).reduce(function (accumulatedEventActions, eventAction) {
|
|
var existingActionValue = accumulatedEventActions[eventAction];
|
|
var actionValue = treatmentEventActions[eventAction];
|
|
// Merge the event action values from different treatments
|
|
if (reflect.isArray(existingActionValue)) {
|
|
// If the action value is not an array, treat it as a bad data and discard it.
|
|
if (reflect.isArray(actionValue)) {
|
|
actionValue.forEach(function (value) {
|
|
if (existingActionValue.indexOf(value) === -1) {
|
|
existingActionValue.push(value);
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
// Currently only have array type and primitive type parameters. Ignore the other types of parameter values.
|
|
if (reflect.isArray(actionValue)) {
|
|
// Clone the array value, to avoid the original array is changed by other treatments.
|
|
accumulatedEventActions[eventAction] = actionValue.slice();
|
|
} else if (
|
|
reflect.isObject(actionValue) ||
|
|
(!reflect.isObject(actionValue) && !reflect.isFunction(actionValue))
|
|
) {
|
|
// object, primitive type, null and undefined
|
|
// set the existing action value with the primitive type value.
|
|
accumulatedEventActions[eventAction] = actionValue;
|
|
}
|
|
}
|
|
|
|
return accumulatedEventActions;
|
|
}, currentTreatmentEventActions);
|
|
}
|
|
}
|
|
|
|
function _updateFieldActions(targetTreatment, sourceTreatment) {
|
|
if (!targetTreatment[TREATMENT_FIELD_ACTIONS]) {
|
|
targetTreatment[TREATMENT_FIELD_ACTIONS] = {};
|
|
}
|
|
updateFieldRulesets(
|
|
targetTreatment,
|
|
sourceTreatment,
|
|
TREATMENT_FIELD_ACTIONS,
|
|
function (targetRules, sourceRules, fieldName) {
|
|
// if the target field rule has the same treatmentType as the source field rule, then return the target field rule to replace its rule props with the source ones.
|
|
// otherwise, all of the source field rules will overwrite all of the target field rules
|
|
if (
|
|
targetRules[fieldName] &&
|
|
targetRules[fieldName].treatmentType === sourceRules[fieldName].treatmentType
|
|
) {
|
|
return targetRules[fieldName];
|
|
} else {
|
|
// if the treatmentType is not the same between field rules, return an empty object to take the latter field rules
|
|
/*
|
|
{
|
|
treatments: [{
|
|
...,
|
|
fieldActions: {
|
|
afield: { treatmentType: 'a', propA: 123 }
|
|
}
|
|
}, {
|
|
...,
|
|
fieldActions: {
|
|
afield: { treatmentType: 'b', propB: 123 }
|
|
}
|
|
}]
|
|
}
|
|
|
|
expected output:
|
|
{
|
|
treatments: [{
|
|
...,
|
|
fieldActions: {
|
|
afield: { treatmentType: 'b', propB: 123 }
|
|
}
|
|
}]
|
|
}
|
|
*/
|
|
return {};
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
var TreatmentGenerator = function TreatmentGenerator(constraintsInstance) {
|
|
// @private
|
|
this._constraintsInstance = constraintsInstance;
|
|
this.treatment = new ActionTreatment(constraintsInstance);
|
|
};
|
|
|
|
/**
|
|
* Combine treatments from multiple profiles
|
|
* @param {Array} ConstraintProfiles the constraint profile names
|
|
* @param {Object} topicConfig An AMP Metrics Config
|
|
* @param {String}(optional) topic defines the AMP Analytics "topic" to look up the constraint profile
|
|
* @returns {Promise} a Promise that returns an Array of combined treatments from multiple constraint profiles
|
|
* @private
|
|
*/
|
|
TreatmentGenerator.prototype._combineTreatments = function _combineTreatments(constraintProfiles, topicConfig, topic) {
|
|
var combinedTreatmentsPromise;
|
|
var buildTreatmentTasks = [];
|
|
|
|
if (reflect.isArray(constraintProfiles)) {
|
|
constraintProfiles.forEach(function (constraintProfile) {
|
|
if (!constraintProfile) {
|
|
return;
|
|
}
|
|
|
|
var profileName = 'treatmentProfiles.' + constraintProfile;
|
|
var constraintsPromise = topicConfig.value(profileName, topic).then(function (constraints) {
|
|
return constraints && constraints.treatments ? constraints.treatments : [];
|
|
});
|
|
buildTreatmentTasks.push(constraintsPromise);
|
|
});
|
|
|
|
combinedTreatmentsPromise = Promise.all(buildTreatmentTasks).then(function (profilesTreatments) {
|
|
var combinedTreatments = [];
|
|
profilesTreatments.forEach(function (profileTreatments) {
|
|
combinedTreatments = combinedTreatments.concat(profileTreatments);
|
|
});
|
|
|
|
return combinedTreatments;
|
|
});
|
|
} else {
|
|
combinedTreatmentsPromise = Promise.resolve([]);
|
|
}
|
|
|
|
return combinedTreatmentsPromise;
|
|
};
|
|
|
|
/**
|
|
* Build the properly constraints for the passed-in event data
|
|
* @param {Object} eventData a dictionary of event data
|
|
* @param {Object} topicConfig An AMP Metrics Config
|
|
* @param {String}(optional) topic defines the AMP Analytics "topic" to look up the constraint profile
|
|
* @return {Object} a set of constraints to apply to this event.
|
|
* returns null (MetricsKit will send the original event) if:
|
|
* 1. no topic config available
|
|
* 2. defaultTreatmentProfile is undefined
|
|
* 3. the profile is found but no treatment matched
|
|
* @throws {TypeError} throws a type error if the topic config contains any invalid element.
|
|
* @throws {SyntaxError} throws a syntax error if:
|
|
* 1. topicConfig.constraintProfiles(topic) is not found in the topic config
|
|
* 2. the treatment configuration is invalid
|
|
* @overridable
|
|
* Constraint rules will be applied in the order they are provided in config.
|
|
* @example
|
|
* Given the following config:
|
|
* metrics: {
|
|
* ...
|
|
* low_res_topic: {
|
|
* defaultTreatmentProfiles: ['iosStores', 'embeddedWeb'],
|
|
* },
|
|
* defaultTreatmentProfiles: ['embeddedWeb']
|
|
* treatmentProfiles: {
|
|
* iosStores: { version: 1, treatments: [ ... ] },
|
|
* embeddedWeb: {
|
|
* version: 2,
|
|
* treatments: [
|
|
* {
|
|
* filters: {
|
|
* eventType: { valueMatches: ['enter', 'exit' ] },
|
|
* isSignedIn: { valueMatches: [true] }
|
|
* },
|
|
* eventActions: {
|
|
* blacklistedFields: ['cookies']
|
|
* }
|
|
* },
|
|
* {
|
|
* filters: {
|
|
* eventType: { valueMatches: [ 'exit' ] }
|
|
* },
|
|
* eventActions: {
|
|
* blacklistedFields: ['cookies', 'pageDetails']
|
|
* },
|
|
* fieldActions: {
|
|
* clientId: {
|
|
* treatmentType: 'idDeres',
|
|
* storageKeyPrefix: 'mtClientId',
|
|
* namespace: 'test',
|
|
* scopeStrategy: 'mainDomain',
|
|
* scopeFieldName: 'https://www.apple.com/products/',
|
|
* tokenSeparator: 'z',
|
|
* lifespan: 86400000
|
|
* }
|
|
* }
|
|
* },
|
|
* {
|
|
* filters: {
|
|
* userType: { valueMatches: ['signedIn'] },
|
|
* actionType: { valueMatches: ['navigate'] }
|
|
* },
|
|
* fieldActions: {
|
|
* os: { blacklisted: true },
|
|
* // round down time to 1 day
|
|
* eventTime: {
|
|
* treatmentType: "numberDeres",
|
|
* precision: 86400000
|
|
* },
|
|
* // remove query params
|
|
* pageUrl: {
|
|
* treatmentType: "urlDeres",
|
|
* scope: 'fullWithoutParams'
|
|
* },
|
|
* // Deres disk available space round down to MB
|
|
* capacityDiskAvailable: {
|
|
* treatmentType: "numberDeres",
|
|
* precision: 1000000, // 1MB
|
|
* },
|
|
* clientId: {
|
|
* treatmentType: 'idDeres',
|
|
* scopeFieldName: 'https://www.apple.com/',
|
|
* lifespan: 3600000
|
|
* }
|
|
* }
|
|
* }
|
|
* ]
|
|
* }
|
|
* },
|
|
* ...
|
|
* }
|
|
*
|
|
* constraintsForEvent({ eventType: 'exit', isSignedIn: true, userType: 'signedIn', actionType: 'navigate', ... }, topicConfig) returns:
|
|
* {
|
|
* eventActions: {
|
|
* blacklistedFields: ['cookies', 'pageDetails'] // from "eventType: { valueMatches: [ 'exit' ] }", override the one of "treatments[0]"
|
|
* },
|
|
* fieldActions: {
|
|
* os: { blacklisted: true },
|
|
* eventTime: {
|
|
* treatmentType: "numberDeres",
|
|
* precision: 86400000
|
|
* },
|
|
* pageUrl: {
|
|
* treatmentType: "urlDeres",
|
|
* scope: 'fullWithoutParams'
|
|
* },
|
|
* capacityDiskAvailable: {
|
|
* treatmentType: "numberDeres",
|
|
* precision: 1000000, // 1MB
|
|
* },
|
|
* clientId: {
|
|
* treatmentType: 'idDeres',
|
|
* storageKeyPrefix: 'mtClientId',
|
|
* namespace: 'test',
|
|
* scopeStrategy: 'mainDomain',
|
|
* scopeFieldName: 'https://www.apple.com/', // override the value from "treatments[1].clientId"
|
|
* tokenSeparator: 'z',
|
|
* lifespan: 3600000 // override the value from "treatments[1].clientId"
|
|
* }
|
|
* }
|
|
* }
|
|
*/
|
|
TreatmentGenerator.prototype.constraintsForEvent = function constraintsForEvent(eventData, topicConfig, topic) {
|
|
if (!topicConfig) {
|
|
return Promise.resolve(null);
|
|
}
|
|
var self = this;
|
|
|
|
// Use Promise.resolve to wrap the constraintProfiles() here in case of the client delegate the constraintProfiles method and returns non-promise value
|
|
return Promise.resolve(topicConfig.constraintProfiles(topic))
|
|
.then(function (constraintProfiles) {
|
|
// Adapt the v1 profile to v2 profiles
|
|
if (!reflect.isDefinedNonNull(constraintProfiles)) {
|
|
return Promise.resolve(topicConfig.constraintProfile(topic)).then(function (constraintProfile) {
|
|
return reflect.isDefinedNonNull(constraintProfile) ? [constraintProfile] : null;
|
|
});
|
|
} else {
|
|
return constraintProfiles;
|
|
}
|
|
})
|
|
.then(function (constraintProfiles) {
|
|
// rdar://71993234 if there is no default treatment profile and the client did not declare a treatment profile, do not modify the event
|
|
if (reflect.isDefinedNonNull(constraintProfiles)) {
|
|
if (!reflect.isArray(constraintProfiles)) {
|
|
throw new TypeError(
|
|
'"constraintProfiles" should be an Array, but got: ' +
|
|
(constraintProfiles ? constraintProfiles.constructor : constraintProfiles)
|
|
);
|
|
}
|
|
return self
|
|
._combineTreatments(constraintProfiles, topicConfig, topic)
|
|
.then(function (combinedTreatments) {
|
|
// rdar://71993234 if the treatments are not found in the topic config
|
|
if (combinedTreatments.length === 0) {
|
|
throw new SyntaxError(
|
|
'The constraintProfiles: ' +
|
|
constraintProfiles.join(', ') +
|
|
' are not found in the topic config'
|
|
);
|
|
}
|
|
return combinedTreatments;
|
|
});
|
|
} else {
|
|
return Promise.resolve([]);
|
|
}
|
|
})
|
|
.then(function (combinedTreatments) {
|
|
var returnTreatments = combinedTreatments.reduce(function (accumulatedTreatment, treatment) {
|
|
if (self.eventMatchesTreatment(eventData, treatment)) {
|
|
accumulatedTreatment = _updateTreatment(accumulatedTreatment, treatment);
|
|
}
|
|
return accumulatedTreatment;
|
|
}, null);
|
|
|
|
return returnTreatments;
|
|
});
|
|
};
|
|
|
|
TreatmentGenerator.prototype.eventMatchesTreatment = function eventMatchesTreatment(eventData, treatment) {
|
|
var filters = treatment[TREATMENT_FILTERS_FIELD];
|
|
|
|
// Fast false for free-form filter since JS does not support it at the moment
|
|
// Applying the treatment to all events if there is no filters to align the behavior with the native implementation.
|
|
if (!reflect.isDefinedNonNull(filters)) {
|
|
return true;
|
|
}
|
|
// Applying the treatment to all events if the value equals to "any"
|
|
if (reflect.isString(filters)) {
|
|
return filters === TREATMENT_FILTERS_ALL;
|
|
}
|
|
|
|
// If the filter element is an empty filter list. We consider it is an incorrect config.
|
|
if (Object.keys(filters).length === 0) {
|
|
throw new SyntaxError('Unable to find the filter in \n' + JSON.stringify(treatment));
|
|
}
|
|
|
|
return Object.keys(filters).every(function (filterField) {
|
|
var fieldFilters = filters[filterField];
|
|
|
|
// Fast false for free-form filter since JS does not support it at the moment
|
|
if (fieldFilters && reflect.isString(fieldFilters)) {
|
|
return false;
|
|
}
|
|
// if a field isn't an object or doesn't have any matchers. We consider this is a bad filter and discard the event
|
|
if (!fieldFilters || !reflect.isObject(fieldFilters) || reflect.isEmptyObject(fieldFilters)) {
|
|
throw new SyntaxError(
|
|
'Invalid filter object for field (' + filterField + ') in \n' + JSON.stringify(treatment)
|
|
);
|
|
}
|
|
// Only return the treatments where all treatments match.
|
|
// Current, only one condition for one field.
|
|
return Object.keys(fieldFilters).every(function (matcherName) {
|
|
var matcherParam = fieldFilters[matcherName];
|
|
|
|
if (matchers[matcherName]) {
|
|
return matchers[matcherName](filterField, eventData, matcherParam);
|
|
} else {
|
|
throw new SyntaxError(
|
|
'Unable to find the filter (' +
|
|
matcherName +
|
|
') for field (' +
|
|
filterField +
|
|
')in \n' +
|
|
JSON.stringify(treatment)
|
|
);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
/*
|
|
* src/config.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2020 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* The constraints config delegate
|
|
* Constraints attach this delegate to the topic config to have constraints features on the topic config
|
|
*/
|
|
var constraintsConfig = {
|
|
/**
|
|
* Return the constraint profile from a config with constraint syntax v1
|
|
* @param {String}(optional) topic defines the AMP Analytics "topic" that this event should be stored under
|
|
* @return {Promise} a Promise that returns the name of the constraint profile from constraint syntax v1 to use
|
|
*/
|
|
constraintProfile: function constraintProfile(topic) {
|
|
return this.value('constraints.defaultProfile', topic);
|
|
},
|
|
|
|
/**
|
|
* Return the constraint profiles from a config with constraint syntax v2
|
|
* @param {String}(optional) topic defines the AMP Analytics "topic" that this event should be stored under
|
|
* @return {Promise} a Promise that returns an array of the names of the constraint profile to use
|
|
*/
|
|
constraintProfiles: function constraintProfiles(topic) {
|
|
return this.value('defaultTreatmentProfiles', topic);
|
|
}
|
|
};
|
|
|
|
/*
|
|
* src/index.js
|
|
* mt-client-constraints
|
|
*
|
|
* Copyright © 2017-2018 Apple Inc. All rights reserved.
|
|
*
|
|
*/
|
|
|
|
function _validateConfig(config) {
|
|
var isValid = true;
|
|
|
|
isValid &= reflect.isDefinedNonNull(config);
|
|
if (isValid) {
|
|
isValid &= !reflect.isEmptyObject(config);
|
|
isValid &= reflect.isFunction(config.initialized);
|
|
isValid &= reflect.isFunction(config.value);
|
|
isValid &= reflect.isFunction(config.constraintProfile);
|
|
}
|
|
|
|
return isValid;
|
|
}
|
|
|
|
/**
|
|
* Attaching config related methods for Constraints
|
|
* @param {Config} topicConfig An AMP Metrics Config
|
|
* @returns {Config} the passed-in config with constraint-related methods attached
|
|
*/
|
|
function connectConstraintConfig(topicConfig) {
|
|
// return the topic config if it has already been attached with the Constraint methods.
|
|
if (reflect.isFunction(topicConfig.constraintProfile) && reflect.isFunction(topicConfig.constraintProfiles)) {
|
|
return topicConfig;
|
|
}
|
|
reflect.attachMethods(topicConfig, constraintsConfig, topicConfig);
|
|
|
|
return topicConfig;
|
|
}
|
|
|
|
/**
|
|
* Supplies the single JavaScript entrypoint to constraint functionality
|
|
* Since JavaScript is prototype-based and not class-based, and doesn't provide
|
|
* an "official" object model, this API is presented as a functional API, but
|
|
* still retains the ability to override and customize functionality via the
|
|
* "setDelegate()" method. In this way, it doesn't carry with it the spare
|
|
* baggage of exposing a bolt-on object model which may differ from a bolt-on
|
|
* (or homegrown) object model already existing in the app.
|
|
* @module
|
|
* @param {Object} topicConfig a topic config
|
|
* @param {Object} delegates
|
|
* @constructor
|
|
*
|
|
* @example
|
|
* import * as delegates from '@amp-metrics/mt-metricskit-delegates-html';
|
|
* import Constraints, { connectConstraintConfig } from '@amp-metrics/mt-client-constraints';
|
|
* import Config from '@amp-metrics/mt-client-config';
|
|
*
|
|
* const topicConfig = new Config('topic');
|
|
* connectConstraintConfig(topicConfig);
|
|
*
|
|
* var eventData = {...};
|
|
* var constraints = new Constraints(topicConfig, delegates);
|
|
* var constrainedEventData = constraints.applyConstraintTreatments(eventData);
|
|
*/
|
|
var Constraints = function Constraints(topicConfig, delegate) {
|
|
if (!_validateConfig(topicConfig)) {
|
|
throw new Error('The topic config is not a valid instance of "mt-client-config".');
|
|
}
|
|
|
|
// @private
|
|
this._isInitialized = false;
|
|
|
|
// @private
|
|
this._topicConfig = topicConfig;
|
|
|
|
/**
|
|
* constraint generator for specific topic config
|
|
* @type {ConstraintGenerator}
|
|
*/
|
|
// @private
|
|
this._constraintGenerator = null;
|
|
|
|
/**
|
|
* system/platform-specific classes
|
|
*/
|
|
this.system = new System();
|
|
|
|
reflect.setDelegates(this.system, delegate || {});
|
|
};
|
|
|
|
/**
|
|
* get constraint generator based on the Constraints' config
|
|
* @returns {Promise} a Promise that returns the active constraint generator
|
|
*/
|
|
Constraints.prototype._getConstraintGenerator = function _getConstraintGenerator() {
|
|
var self = this;
|
|
|
|
if (this._constraintGenerator) {
|
|
return Promise.resolve(this._constraintGenerator);
|
|
} else {
|
|
return this._topicConfig.value('treatmentProfiles').then(function (treatmentConfig) {
|
|
if (reflect.isDefinedNonNull(treatmentConfig)) {
|
|
self._constraintGenerator = new TreatmentGenerator(self);
|
|
} else {
|
|
self._constraintGenerator = new LegacyConstraintGenerator(self);
|
|
}
|
|
return self._constraintGenerator;
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Build constraints with the eventData
|
|
* @param {Object} eventData a dictionary of event data
|
|
* @param {String}(optional) topic defines the AMP Analytics "topic" that this event should be stored under
|
|
* @return {Promise} a Promise that returns a set of constraints to apply to this event
|
|
* @throws {SyntaxError/TypeError} throws a type error if the topic config contains any invalid element.
|
|
*/
|
|
Constraints.prototype.constraintsForEvent = function constraintsForEvent(eventData, topic) {
|
|
var self = this;
|
|
return this._getConstraintGenerator().then(function (constraintGenerator) {
|
|
return constraintGenerator.constraintsForEvent(eventData, self._topicConfig, topic);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Apply the given eventData with associated constraints
|
|
* @param {Object} eventData a dictionary of event data
|
|
* @param {Object}(optional) constraints a set of constraints to apply to this event
|
|
* @returns {Promise} a Promise that returns the performed event Data with the given constraints or null if the event is blacklisted or should be discard
|
|
*/
|
|
Constraints.prototype.applyConstraintTreatments = function applyConstraints(eventData, constraints) {
|
|
var constraintsPromise = constraints ? Promise.resolve(constraints) : this.constraintsForEvent(eventData);
|
|
var self = this;
|
|
|
|
return Promise.all([constraintsPromise, this._getConstraintGenerator()])
|
|
.then(function (output) {
|
|
var constraints = output[0];
|
|
var constraintGenerator = output[1];
|
|
return constraintGenerator.treatment.applyConstraints(eventData, constraints);
|
|
})
|
|
.catch(function (e) {
|
|
self.system.logger.warn('An error occurred while applying constraints: ' + e.message || e);
|
|
return null;
|
|
});
|
|
};
|
|
|
|
export default Constraints;
|
|
export { connectConstraintConfig };
|