summaryrefslogtreecommitdiff
path: root/shared/metrics-8/node_modules/@amp-metrics/mt-client-constraints/dist
diff options
context:
space:
mode:
authorrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
committerrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
commitbce557cc2dc767628bed6aac87301a1be7c5431b (patch)
treeb51a051228d01fe3306cd7626d4a96768aadb944 /shared/metrics-8/node_modules/@amp-metrics/mt-client-constraints/dist
init commit
Diffstat (limited to 'shared/metrics-8/node_modules/@amp-metrics/mt-client-constraints/dist')
-rw-r--r--shared/metrics-8/node_modules/@amp-metrics/mt-client-constraints/dist/mt-client-constraints.esm.js3103
1 files changed, 3103 insertions, 0 deletions
diff --git a/shared/metrics-8/node_modules/@amp-metrics/mt-client-constraints/dist/mt-client-constraints.esm.js b/shared/metrics-8/node_modules/@amp-metrics/mt-client-constraints/dist/mt-client-constraints.esm.js
new file mode 100644
index 0000000..c72d602
--- /dev/null
+++ b/shared/metrics-8/node_modules/@amp-metrics/mt-client-constraints/dist/mt-client-constraints.esm.js
@@ -0,0 +1,3103 @@
+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&param2=def&param3=ghi#someHash')
+ * // returns 'https://itunes.apple.com:80/music?param1=abc&param2=def&param3=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&param2=def&param3=ghi#someHash', [
+ * {
+ * searchPattern: 'music',
+ * replaceVal: 'm'
+ * }
+ * ])
+ * // returns 'https://itunes.apple.com:80/m?param1=abc&param2=def&param3=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 };