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 };