From bce557cc2dc767628bed6aac87301a1be7c5431b Mon Sep 17 00:00:00 2001 From: rxliuli Date: Tue, 4 Nov 2025 05:03:50 +0800 Subject: init commit --- .../dist/mt-client-constraints.esm.js | 3103 ++++++++++++++++++++ 1 file changed, 3103 insertions(+) create mode 100644 shared/metrics-8/node_modules/@amp-metrics/mt-client-constraints/dist/mt-client-constraints.esm.js (limited to 'shared/metrics-8/node_modules/@amp-metrics/mt-client-constraints') 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¶m2=def¶m3=ghi#someHash') + * // returns 'https://itunes.apple.com:80/music?param1=abc¶m2=def¶m3=ghi' + */ +function withoutHash(aUrl) { + var url = aUrl || ''; + return url.split('#')[0]; +} + +/** + * Returns the url with all of the string replacements applied + * @param {String} aUrl + * @param {Object} replacements The list of replacements that should be applied on the url + * @param {String} replacements.searchPattern A stringified regex pattern to search for + * @param {String} replacements.replaceVal The string to replace the match with + * @param {String} replacements.flags Regex flags to include with the search pattern (ex: 'g') + * @return {String} The url with all replacements applied + * @overridable + * + * @example + * withReplacements('https://itunes.apple.com:80/music?param1=abc¶m2=def¶m3=ghi#someHash', [ + * { + * searchPattern: 'music', + * replaceVal: 'm' + * } + * ]) + * // returns 'https://itunes.apple.com:80/m?param1=abc¶m2=def¶m3=ghi#someHash' + * + * withReplacements('https://apple.com/1234', [ + * { + * searchPattern: '\d', + * replaceVal: 'X', + * flags: 'g' + * } + * ]) + * // returns 'https://apple.com/XXXX' + */ +function withReplacements(aUrl, replacements) { + var url = aUrl || ''; + var urlReplacements = replacements || []; + + var replacedUrl = urlReplacements.reduce(function (url, replacement) { + var searchPattern = new RegExp(replacement.searchPattern, replacement.flags); + var replaceVal = replacement.replaceVal; + return url.replace(searchPattern, replaceVal); + }, url); + + return replacedUrl; +} + +/* + * src/utils/id_generator.js + * mt-client-constraints + * + * Copyright © 2023 Apple Inc. All rights reserved. + * + */ + +var ID_SEPARATOR = '-'; +var DEFAULT_GENERATED_ID_SEPARATOR = 'z'; + +/** + * @param {Object} options includes - information about how to generate an ID + * @param {Number} options.idVersion - the version of the ID + * @param {Number} options.time - the time to be a part of the ID (optional) + * @param {String} options.generatedIdSeparator - a token-separated hex string of metadata to attach to a ID (optional) default to "z" + * @return {String} a generated ID + */ +function generateId(options) { + if (!reflect.isDefinedNonNull(options) || !reflect.isInteger(options.idVersion)) { + return '0'; + } + var uuid = string.uuid(); + var generatedIdSeparator = options.generatedIdSeparator || DEFAULT_GENERATED_ID_SEPARATOR; + var idString = generatedIdMetadata(options) + ID_SEPARATOR + uuid || ''; + + var convertedIdString = idString + .split(ID_SEPARATOR) + .map(function (segment) { + var segmentAsNumber = parseInt(segment, 16); + return string.convertNumberToBaseAlphabet(segmentAsNumber, string.base61Alphabet); + }) + .join(generatedIdSeparator); + + return convertedIdString; +} + +/** + * @param {Object} options includes - information about how to generate an ID + * @param {Number} options.idVersion - the version of the ID + * @param {Number} options.time - the time to be a part of the ID (optional) + * @return {String} a token-separated hex string of metadata to attach to a ID, + */ +function generatedIdMetadata(options) { + var parameters = [options.idVersion]; + + if (options.time) { + parameters.push(options.time); + } + + return parameters + .map(function (param) { + return param.toString(16); + }) + .join(ID_SEPARATOR); +} + +/* + * src/field_actions/id_action/time_based_action.js + * mt-client-constraints + * + * Copyright © 2021 Apple Inc. All rights reserved. + * + */ + +/** + * The time-based ID generating strategy + * The ID value will change after its lifespan expires + */ +function constrainedValue(idString, idRules, eventData, fieldName) { + var storageKey = this.storageKey(fieldName, eventData, idRules); + var environment = this._constraintsInstance.system.environment; + var idData = storage.objectFromStorage(environment.localStorageObject(), storageKey) || {}; + + idData.value = this.idString(idData, idRules); + if ( + this.rulesHaveLifespan(idRules) && + (!reflect.isNumber(idData.expirationTime) || this.timeExpired(idData.expirationTime)) + ) { + idData.expirationTime = this.expirationTime(idRules.lifespan); + } + + storage.saveObjectToStorage(environment.localStorageObject(), storageKey, idData); + idString = idData.value; + + return idString; +} + +/* + * src/field_actions/id_action/session_time_based_action.js + * mt-client-constraints + * + * Copyright © 2021 Apple Inc. All rights reserved. + * + */ + +// @private +// A global cache storage to store the served clientIds by the clientId storageKey +// Make it as a global variable to ensure the cached clientId can be shared between multiple MK instances +var _sessionIdCache = {}; + +/** + * The user-session-based + time-based ID generating strategy + * When the id getting expired, this function will return a consistent ID until the current user session ends, even if the ID is scheduled to expire in the middle of the session + */ +function constrainedValue$1(idString, idRules, eventData, fieldName) { + var storageKey = this.storageKey(fieldName, eventData, idRules); + var returnedIdString = _sessionIdCache[storageKey]; + + if (!returnedIdString) { + returnedIdString = constrainedValue.apply(this, arguments); + _sessionIdCache[storageKey] = returnedIdString; + } + return returnedIdString; +} + +/* + * src/field_actions/id_action/id_action.js + * mt-client-constraints + * + * Copyright © 2020 Apple Inc. All rights reserved. + * + */ + +var STORAGE_KEY_SEPARATOR = '_'; +var MT_ID_NAMESPACE = 'mtId'; + +var IdAction = function IdAction() { + Base$1.apply(this, arguments); +}; + +IdAction.prototype = Object.create(Base$1.prototype); +IdAction.prototype.constructor = IdAction; + +/* + * Possible strategies that can be used to scope an ID value + */ +IdAction.prototype.SCOPE_STRATEGIES = { + ALL: 'all', + MAIN_DOMAIN: 'mainDomain' +}; + +/** + * @param {Object} (optional) idRules includes information about when to expire the ID + * @return {Boolean} + */ +IdAction.prototype.rulesHaveLifespan = function rulesHaveLifespan(idRules) { + idRules = idRules || {}; + + return reflect.isNumber(idRules.lifespan); +}; + +/** + * @param {Number} (optional) lifespan the amount of time, in milliseconds, that an ID should be valid for + * @return {Number} a timestamp in ms since epoch, or null if no lifespan was provided + */ +IdAction.prototype.expirationTime = function expirationTime(lifespan) { + return lifespan ? Date.now() + lifespan : null; +}; + +/** + * @param {String} fieldName - name of the field being field_actions in eventData + * @param {Object} eventData a dictionary of event data + * @param {Object} idRules includes information about how to namespace/scope the id + * @return {String} the key that id data should be stored under + * @example + * (storageKeyPrefix ? storageKeyPrefix : mtId_)__(scopeStrategy ? : '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 property with field rules in it. + * @param {Object} newRules - an object contains an 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} 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)__(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: __salt_ + * @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} The hashed value on the top of provided value with the stored salt (rotated for every 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 }; -- cgit v1.2.3