Files
apps.apple.com/shared/metrics-8/node_modules/@amp-metrics/mt-client-constraints/dist/mt-client-constraints.esm.js
2025-11-04 05:03:50 +08:00

3104 lines
119 KiB
JavaScript

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