___TERMS_OF_SERVICE___ By creating or modifying this file you agree to Google Tag Manager's Community Template Gallery Developer Terms of Service available at https://developers.google.com/tag-manager/gallery-tos (or such other URL as Google may provide), as modified from time to time. ___INFO___ { "type": "TAG", "id": "cvt_temp_public_id", "version": 1, "securityGroups": [], "displayName": "MoEngage", "categories": ["MARKETING"], "brand": { "id": "brand_dummy", "displayName": "", "thumbnail": "data:image/png;base64,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\u003d\u003d" }, "description": "Tag that sends events to MoEngage from Snowplow, GAv4 or other clients.", "containerContexts": [ "SERVER" ] } ___TEMPLATE_PARAMETERS___ [ { "type": "SELECT", "name": "dataCenter", "displayName": "Select data center", "macrosInSelect": false, "selectItems": [ { "value": 1, "displayValue": "Dashboard-01 (DC-01)" }, { "value": 2, "displayValue": "Dashboard-02 (DC-02)" }, { "value": 3, "displayValue": "Dashboard-03 (DC-03)" }, { "value": 4, "displayValue": "Dashboard-04 (DC-04)" } ], "simpleValueType": true, "alwaysInSummary": true, "help": "You can find more details about data centers here: https://help.moengage.com/hc/en-us/articles/360057030512-Data-Centers-in-MoEngage", "valueValidators": [ { "type": "NON_EMPTY" }, { "type": "POSITIVE_NUMBER" } ] }, { "type": "TEXT", "name": "apiID", "displayName": "API ID", "simpleValueType": true, "alwaysInSummary": true, "help": "The API ID for your MoEngage account is available on the MoEngage Dashboard \u003e\u003e Settings \u003e\u003e APIs \u003e\u003e General Settings \u003e\u003e APP ID", "valueValidators": [ { "type": "NON_EMPTY" } ] }, { "type": "TEXT", "name": "apiKey", "displayName": "API Key", "simpleValueType": true, "alwaysInSummary": true, "help": "The API Key for your MoEngage account is available on the MoEngage Dashboard \u003e\u003e Settings \u003e\u003e APIs \u003e\u003e General Settings \u003e\u003e APP Key", "valueValidators": [ { "type": "NON_EMPTY" } ] }, { "type": "GROUP", "name": "userIdentifier", "displayName": "User Identifier", "groupStyle": "ZIPPY_OPEN", "subParams": [ { "type": "SELECT", "name": "userIdentifierType", "displayName": "", "macrosInSelect": false, "selectItems": [ { "value": "userIdentifierVariable", "displayValue": "From Variable" }, { "value": "userIdentifierEvent", "displayValue": "From Event Property" } ], "simpleValueType": true, "defaultValue": "userIdentifierVariable" }, { "type": "TEXT", "name": "userID", "displayName": "", "simpleValueType": true, "alwaysInSummary": true, "valueValidators": [ { "type": "NON_EMPTY" } ] } ], "help": "Select the identifier to map users in MoEngage" }, { "type": "GROUP", "name": "snowplowEventMapping", "displayName": "Snowplow Event Mapping Options", "groupStyle": "ZIPPY_CLOSED", "subParams": [ { "type": "GROUP", "name": "atomicProp", "displayName": "Snowplow atomic properties", "groupStyle": "ZIPPY_OPEN", "subParams": [ { "type": "CHECKBOX", "name": "includeAllAtomicProperties", "checkboxText": "Include all atomic properties", "simpleValueType": true, "defaultValue": true, "alwaysInSummary": true } ] }, { "type": "GROUP", "name": "selfDescribingEvent", "displayName": "Snowplow Self Describing Event", "groupStyle": "ZIPPY_OPEN", "subParams": [ { "type": "CHECKBOX", "name": "includeSelfDescribingEvent", "checkboxText": "Include Self Describing event", "simpleValueType": true, "help": "Indicates if a Snowplow Self Describing event should be in the event object.", "defaultValue": true, "alwaysInSummary": true } ] }, { "type": "GROUP", "name": "entityRules", "displayName": "Snowplow Event Context Rules", "groupStyle": "ZIPPY_OPEN", "subParams": [ { "type": "SELECT", "name": "includeEntities", "displayName": "Include Snowplow Entities in event object", "macrosInSelect": false, "selectItems": [ { "value": "all", "displayValue": "All" }, { "value": "none", "displayValue": "None" } ], "simpleValueType": true, "defaultValue": "none" }, { "type": "SIMPLE_TABLE", "name": "entityMappingRules", "displayName": "Snowplow Entities to Add/Edit mapping", "simpleTableColumns": [ { "defaultValue": "", "displayName": "Entity Name", "name": "key", "type": "TEXT", "valueHint": "The entity name from the original event (e.g. `web_page` from `iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0`)", "isUnique": true, "valueValidators": [ { "type": "NON_EMPTY" } ] }, { "defaultValue": "event_object", "displayName": "Include in event object or user attributes object", "name": "propertiesObjectToPopulate", "type": "SELECT", "selectItems": [ { "value": "event_object", "displayValue": "event_object" }, { "value": "user_attributes_object", "displayValue": "user_attributes_object" } ], "valueValidators": [ { "type": "NON_EMPTY" } ] }, { "defaultValue": "control", "displayName": "Apply to all versions", "name": "version", "type": "SELECT", "selectItems": [ { "value": "control", "displayValue": "False" }, { "value": "free", "displayValue": "True" } ], "valueValidators": [ { "type": "NON_EMPTY" } ] } ], "alwaysInSummary": false, "help": "Specify the Entity name and append `x-sp-` in all the entities before mapping." }, { "type": "SIMPLE_TABLE", "name": "entityExclusionRules", "displayName": "Snowplow Entities to Exclude", "simpleTableColumns": [ { "defaultValue": "", "displayName": "Entity Name", "name": "key", "type": "TEXT", "isUnique": true, "valueValidators": [ { "type": "NON_EMPTY" } ] }, { "defaultValue": "control", "displayName": "Apply to all versions", "name": "version", "type": "SELECT", "selectItems": [ { "value": "control", "displayValue": "False" }, { "value": "free", "displayValue": "True" } ], "valueValidators": [ { "type": "NON_EMPTY" } ] } ], "enablingConditions": [ { "paramName": "includeEntities", "paramValue": "all", "type": "EQUALS" } ], "help": "Specify the Entity name you want to exclude from the MoEngage event. Additionally, you can also set whether the exclusion applies to all versions of the entity." } ] } ] }, { "type": "GROUP", "name": "otherEventMapping", "displayName": "Additional Event Mapping Options", "groupStyle": "ZIPPY_CLOSED", "subParams": [ { "type": "GROUP", "name": "commonEventPropertyRules", "displayName": "Event Property Rules", "groupStyle": "ZIPPY_OPEN", "subParams": [ { "type": "CHECKBOX", "name": "includeCommonEventProperties", "checkboxText": "Include common event properties", "simpleValueType": true, "alwaysInSummary": true, "defaultValue": true, "help": "Include the event properties from the common event definition in MoEngage event object." }, { "type": "SIMPLE_TABLE", "name": "eventMappingRules", "displayName": "Additional Event Property Mapping Rules", "simpleTableColumns": [ { "defaultValue": "", "displayName": "Event Property Key", "name": "key", "type": "TEXT" }, { "defaultValue": "", "displayName": "MoEngage Mapped Key (optional)", "name": "mappedKey", "type": "TEXT" } ], "alwaysInSummary": false, "help": "Specify the Property Key from the GTM Event, and then key you could like to map it to or leave the mapped key blank to keep the same name. These keys will populate the MoEngage event object." } ] }, { "type": "GROUP", "name": "commonUserPropertyRules", "displayName": "User Property Rules", "groupStyle": "ZIPPY_OPEN", "subParams": [ { "type": "CHECKBOX", "name": "includeCommonUserProperties", "checkboxText": "Include common user properties", "simpleValueType": true, "alwaysInSummary": true, "defaultValue": true, "help": "Include the user_data properties from the common event definition as MoEngage user attributes." }, { "type": "SIMPLE_TABLE", "name": "userMappingRules", "displayName": "Additional User Property Mapping Rules", "simpleTableColumns": [ { "defaultValue": "", "displayName": "User Property Key", "name": "key", "type": "TEXT" }, { "defaultValue": "", "displayName": "MoEngage Mapped Key (optional)", "name": "mappedKey", "type": "TEXT" } ], "alwaysInSummary": false, "help": "Specify the Property Key from the GTM Event, and the key you could like to map it to or leave the mapped key blank to keep the same name. These keys will populate the MoEngage user attributes object." } ] } ] }, { "type": "GROUP", "name": "additionalUnmappedEventData", "displayName": "Additional Event Data", "groupStyle": "ZIPPY_CLOSED", "subParams": [ { "type": "SIMPLE_TABLE", "name": "additionalEventKeyValue", "displayName": "Additional event properties mapping", "simpleTableColumns": [ { "defaultValue": "", "displayName": "Key", "name": "key", "type": "TEXT" }, { "defaultValue": "", "displayName": "Value", "name": "value", "type": "TEXT" } ], "help": "Directly send these key value pairs in the payload using GTM variables" } ], "help": "Directly send these key value pairs in the payload using GTM variables" }, { "type": "GROUP", "name": "additionalUnmappedUserData", "displayName": "Additional User Data", "groupStyle": "ZIPPY_CLOSED", "subParams": [ { "type": "SIMPLE_TABLE", "name": "additionalUserKeyValue", "displayName": "Additional user attributes mapping", "simpleTableColumns": [ { "defaultValue": "", "displayName": "Key", "name": "key", "type": "TEXT" }, { "defaultValue": "", "displayName": "Value", "name": "value", "type": "TEXT" } ], "help": "Directly send these key value pairs in the payload using GTM variables" } ], "help": "Directly send these key value pairs in the payload using GTM variables" }, { "type": "GROUP", "name": "advancedProperties", "displayName": "Advanced Event Settings", "groupStyle": "ZIPPY_CLOSED", "subParams": [ { "type": "TEXT", "name": "eventNameOverride", "displayName": "Event Name Override", "simpleValueType": true, "help": "Use this option to override the name of the MoEngage event object. Leave blank to inherit from common event properties." }, { "type": "TEXT", "name": "eventTime", "displayName": "Event time property", "simpleValueType": true, "help": "Specify the client event property to populate the event time (in ISO-8601 format) or leave it empty to use the current time." } ] }, { "type": "GROUP", "name": "logsGroup", "displayName": "Log settings", "groupStyle": "ZIPPY_CLOSED", "subParams": [ { "type": "RADIO", "name": "logType", "displayName": "", "radioItems": [ { "value": "no", "displayValue": "Do not log" }, { "value": "debug", "displayValue": "Log to console during debug and preview" }, { "value": "always", "displayValue": "Always log to console" } ], "simpleValueType": true, "defaultValue": "debug" } ] } ] ___SANDBOXED_JS_FOR_SERVER___ const getAllEventData = require('getAllEventData'); const getEventData = require('getEventData'); const getContainerVersion = require('getContainerVersion'); const getRequestHeader = require('getRequestHeader'); const toBase64 = require('toBase64'); const makeTableMap = require('makeTableMap'); const getType = require('getType'); const getTimestampMillis = require('getTimestampMillis'); const sendHttpRequest = require('sendHttpRequest'); const logToConsole = require('logToConsole'); const JSON = require('JSON'); const Object = require('Object'); const Math = require('Math'); const createRegex = require('createRegex'); // **************************** Constants Start **************************** const tagName = 'MoEngage'; // **************************** Constants End **************************** // **************************** Snowplow Helpers Start **************************** /** * Returns whether a string is upper case. * * @param {string} value - The string to check * @returns {boolean} */ const isUpper = (value) => { return value === value.toUpperCase() && value !== value.toLowerCase(); }; /** * Converts a string to snake case. * * @param {string} value - The string to convert * @returns {string} The converted string */ const toSnakeCase = (value) => { let result = ''; let previousChar; for (var i = 0; i < value.length; i++) { let currentChar = value.charAt(i); if (isUpper(currentChar) && i > 0 && previousChar !== '_') { result = result + '_' + currentChar; } else { result = result + currentChar; } previousChar = currentChar; } return result; }; /** * Merges objects. * * @param {Object[]} args - The array of objects to merge * @returns {Object} The resulting object */ const mergeObjects = (args) => { let target = {}; const addToTarget = (obj) => { for (let prop in obj) { if (obj.hasOwnProperty(prop)) { target[prop] = obj[prop]; } } }; for (let i = 0; i < args.length; i++) { addToTarget(args[i]); } return target; }; /** * Returns object keys in an array * * @param Object - Object * @returns Array - List of keys */ const getKeysOf = (obj) => { const keys = []; for (let prop in obj) { if (obj.hasOwnProperty(prop)) { keys.push(prop); } } return keys; }; /** * Flattens a nested object to root level * For same key last occurence is given as final value * @example * // returns { a: [1,2,3] } * flattenObject({ a: [1,2,3] }); * @example * // returns { d:3,e:2,b:4 } * flattenObject({a:{b:{c:{d:1,e:2}, d: 3}},b: 4}); * @example * // returns { d:[1,2,3,4],e:2,b:4 } * flattenObject({a:{b:{c:{d:[1,2,3,4],e:2}}},b: 4}); * @param Object - Object * @returns Object - Flattened object */ const flattenObject = (obj) => { if (obj) { return getKeysOf(obj).reduce((acc, k) => { if ( getType(obj[k]) === 'object' || ( getType(obj[k]) === 'array' && getType(obj[k][0]) !== 'number' && getType(obj[k][0]) !== 'string' ) ) { acc = mergeObjects([acc, flattenObject(obj[k], k)]); } else { acc[k] = obj[k]; } return acc; }, {}); } return {}; }; /** * Filters out invalid rules to avoid unintended behavior. * (e.g. version control being ignored if version num is not included in name) * Assumes that a rule contains 'key' and 'version' properties. * * @param {Object[]} rules - The array of rules * @returns {Object[]} The valid rules */ const cleanRules = (rules) => { const lastNumRexp = createRegex('[0-9]$'); return rules.filter((row) => { if (row.version === 'control') { return !!row.key.match(lastNumRexp); } return true; }); }; /** * Parses a Snowplow schema to the expected major version format, * also prefixed so as to match the contexts' output of the Snowplow Client. * * @param schema {string} - The input schema * @returns {string} The expected output client event property */ const parseSchemaToMajorKeyValue = (schema) => { if (schema.indexOf('x-sp-contexts_') === 0) return schema; if (schema.indexOf('contexts_') === 0) return 'x-sp-' + schema; if (schema.indexOf('iglu:') === 0) { const rexp = createRegex('[./]', 'g'); let fixed = schema .replace('iglu:', '') .replace('jsonschema/', '') .replace(rexp, '_'); for (let i = 0; i < 2; i++) { fixed = fixed.substring(0, fixed.lastIndexOf('-')); } return 'x-sp-contexts_' + toSnakeCase(fixed).toLowerCase(); } return schema; }; /** * Removes the major version part from a schema reference if exists. * @example * // returns 'com_acme_test' * mkVersionFree('com_acme_test_1') * @example * // returns 'com_acme_test' * mkVersionFree('com_acme_test') * * @param {string} schemaRef - The schema * @returns {string} */ const mkVersionFree = (schemaRef) => { const versionRexp = createRegex('_[0-9]+$'); return schemaRef.replace(versionRexp, ''); }; /** * Cleans a name from the GTM-SS Snowplow prefix ('x-sp-'). * * @param {string} prop - The property name * @returns {string} The property name with the GTM-SS Snowplow prefix removed. */ const cleanPropertyName = (prop) => prop.replace('x-sp-', ''); /** * Parses the entity inclusion rules from the tag configuration. * * @param {Object} tagConfig - The tag configuration * @returns {Object[]} */ const parseEntityRules = (tagConfig) => { const rules = tagConfig.entityMappingRules; if (rules) { const validRules = cleanRules(rules); const parsedRules = validRules.map((row) => { const parsedKey = parseSchemaToMajorKeyValue(row.key); return { ref: row.version === 'control' ? parsedKey : mkVersionFree(parsedKey), parsedKey: parsedKey, mappedKey: cleanPropertyName(parsedKey), target: row.propertiesObjectToPopulate, version: row.version, }; }); return parsedRules; } return []; }; /** * Parses the entity exclusion rules from the tag configuration. * * @param {Object} tagConfig - The tag configuration * @returns {Object[]} */ const parseEntityExclusionRules = (tagConfig) => { const rules = tagConfig.entityExclusionRules; if (rules) { const validRules = cleanRules(rules); const excludedEntities = validRules.map((row) => { const entityRef = parseSchemaToMajorKeyValue(row.key); return { ref: row.version === 'control' ? entityRef : mkVersionFree(entityRef), version: row.version, }; }); return excludedEntities; } return []; }; /** * Given a list of entity references and an entity name, * returns the index of a matching reference. * Matching reference means whether the entity name starts with ref. * @example * // returns 0 * getReferenceIdx('com_test_test_1', ['com_test_test_1']); * @example * // returns 0 * getReferenceIdx('com_test_test_1', ['com_test_test']); * @example * // returns -1 * getReferenceIdx('com_test_test_1', ['com_test_test_2']); * @example * // returns -1 * getReferenceIdx('com_test_test', ['com_test_test_fail']); * @example * // returns -1 * getReferenceIdx('com_test_test_fail', ['com_test_test']); * * @param {string} entity - The entity name to match * @param {string[]} refsList - An array of references * @returns {integer} */ const getReferenceIdx = (entity, refsList) => { const versionFreeEntity = mkVersionFree(entity); for (let i = 0; i < refsList.length; i++) { const okControl = entity.indexOf(refsList[i]) === 0; const okFree = versionFreeEntity === mkVersionFree(refsList[i]); if (okControl && okFree) { return i; } } return -1; }; /** * Given the inclusion rules and the excluded entity references, * returns the final entity mapping rules. * * @param {Object} tagConfig - The tag configuration * @returns {Object[]} */ const finalizeEntityRules = (inclusionRules, excludedRefs) => { const finalEntities = inclusionRules.filter((row) => { const refIdx = getReferenceIdx(row.ref, excludedRefs); return refIdx < 0; }); return finalEntities; }; /** * Returns whether a property name is a Snowplow self-describing event property. * * @param {string} prop - The property name * @returns {boolean} */ const isSpSelfDescProp = (prop) => { return prop.indexOf('x-sp-self_describing_event_') === 0; }; /** * Returns whether a property name is a Snowplow context/entity property. * * @param {string} prop - The property name * @returns {boolean} */ const isSpContextsProp = (prop) => { return prop.indexOf('x-sp-contexts_') === 0; }; /** * Returns whether a property name is a Snowplow event property. * * @param {string} prop - The property name * @returns {boolean} */ const isSpEventProp = (prop) => { const excludeKeys = [ 'x-sp-tp2', 'x-sp-contexts', 'x-sp-self_describing_event', ]; return prop.indexOf('x-sp-') === 0 && excludeKeys.indexOf(prop) < 0; }; /** * Returns whether a property name is a Snowplow atomic property. * * @param {string} prop - The property name * @returns {boolean} */ const isSpAtomicProp = (prop) => { return ( isSpEventProp(prop) && !isSpSelfDescProp(prop) && !isSpContextsProp(prop) ); }; /** * Modifies the respective objects to populate according to Snowplow Event Context Rules. * * @param {Object} eventData - The client event object * @param {Object} tagConfig - The tag configuration object * @param {Object} eventProperties - The object to populate for `event_object` * @param {Object} userAttributes - The object to populate for `user_attributes_object` * @returns {undefined} */ const parseCustomEventAndEntities = ( eventData, tagConfig, eventProperties, userAttributes ) => { const inclusionRules = parseEntityRules(tagConfig); const exclusionRules = parseEntityExclusionRules(tagConfig); const excludedRefs = exclusionRules.map((r) => r.ref); const finalEntityRules = finalizeEntityRules(inclusionRules, excludedRefs); const finalEntityRefs = finalEntityRules.map((r) => r.ref); for (let prop in eventData) { if (eventData.hasOwnProperty(prop)) { const cleanPropName = cleanPropertyName(prop); if (isSpSelfDescProp(prop) && tagConfig.includeSelfDescribingEvent) { if (!isEmpty(eventData[prop])) { for (let key in eventData[prop]) { if (eventData[prop].hasOwnProperty(key)) { eventProperties[key] = eventData[prop][key]; } } } continue; } if (isSpContextsProp(prop)) { if (getReferenceIdx(prop, excludedRefs) >= 0) { continue; } const ctxVal = eventData[prop]; const refIdx = getReferenceIdx(prop, finalEntityRefs); if (refIdx >= 0) { const rule = finalEntityRules[refIdx]; const target = rule.target === 'event_object' ? eventProperties : userAttributes; target[rule.mappedKey] = ctxVal; } else { if (tagConfig.includeEntities === 'none') { continue; } // here includedEntities is 'all' and prop is not excluded eventProperties[cleanPropName] = ctxVal; } } if (isSpAtomicProp(prop) && tagConfig.includeAllAtomicProperties) { eventProperties[cleanPropName] = eventData[prop]; } } } }; // **************************** Snowplow Helpers End **************************** // **************************** Helpers Start **************************** /** * Assumes logType argument is string. * Determines if logging is enabled. * * @param {string} logType - The logType set ('no', 'debug', 'always') * @returns {boolean} Whether logging is enabled */ const determineIsLoggingEnabled = (logType) => { const containerVersion = getContainerVersion(); const isDebugMode = !!( containerVersion && (containerVersion.debugMode || containerVersion.previewMode) ); if (!logType) { return isDebugMode; } if (data.logType === 'no') { return false; } if (data.logType === 'debug') { return isDebugMode; } return data.logType === 'always'; }; /** * Creates the log message and logs it to console. * * @param {string} typeName - The type of log ('Message', 'Request', 'Response') * @param {Object} stdInfo - The standard info for all logs (Name, Type, TraceId, EventName) * @param {Object} logInfo - An object including information for the specific log type * @returns {undefined} */ const doLogging = (typeName, stdInfo, logInfo) => { const logMessage = { Name: stdInfo.tagName, Type: typeName, TraceId: stdInfo.traceId, EventName: stdInfo.eventName, }; switch (typeName) { case 'Message': logMessage.Message = logInfo.msg; break; case 'Request': logMessage.RequestMethod = logInfo.requestMethod; logMessage.RequestUrl = logInfo.requestUrl; logMessage.RequestHeaders = logInfo.requestHeaders; logMessage.RequestBody = logInfo.requestBody; break; case 'Response': logMessage.ResponseStatusCode = logInfo.responseStatusCode; logMessage.ResponseHeaders = logInfo.responseHeaders; logMessage.ResponseBody = logInfo.responseBody; break; default: // do nothing return; } logToConsole(JSON.stringify(logMessage)); }; /** * Left-pads an integer with zeros. Assumes a non-negative integer. * * @param {integer} x - The integer to pad * @param {integer} totalLen - The desired total length * @returns {string} The resulting string after padding with zeros */ const pad = (x, totalLen) => { const s = x.toString(); if (s.length < totalLen) { return pad('0' + s, totalLen); } return s; }; /** * Given the time components, returns the ISO-8601 time code. * * @param {integer} yr - Year * @param {integer} mon - Month * @param {integer} day - Day * @param {integer} hrs - Hours * @param {integer} mins - Minutes * @param {integer} secs - Seconds * @param {integer} msecs - Milliseconds * @returns {string} */ const formatISO = (yr, mon, day, hrs, mins, secs, msecs) => { const isoDate = [yr.toString(), pad(mon, 2), pad(day, 2)].join('-'); const isoTime = [pad(hrs, 2), pad(mins, 2), pad(secs, 2)].join(':'); return isoDate + 'T' + isoTime + '.' + pad(msecs, 3) + 'Z'; }; /** * Transforms a unix timestamp in milliseconds to ISO-8601 time code. * Ref: https://howardhinnant.github.io/date_algorithms.html#civil_from_days * * @param {integer} unixMillis - The unix time in millis * @returns {string} The ISO time code */ const unixMillisToISO = (unixMillis) => { const totalSecs = Math.floor(unixMillis / 1000); const z = Math.floor(totalSecs / 86400) + 719468; const era = Math.floor((z >= 0 ? z : z - 146096) / 146097); const doe = z - era * 146097; const yoe = Math.floor( (doe - (Math.floor(doe / 1460) + Math.floor(doe / 36524) - Math.floor(doe / 146096))) / 365 ); const y = yoe + era * 400; const doy = doe - (365 * yoe + Math.floor(yoe / 4) - Math.floor(yoe / 100)); const mp = Math.floor((5 * doy + 2) / 153); const prevDay = doy - Math.floor((153 * mp + 2) / 5); const day = prevDay + 1; const month = mp < 10 ? mp + 3 : mp - 9; const year = month <= 2 ? y + 1 : y; const daySecs = totalSecs % 86400; const dayMins = Math.floor(daySecs / 60); const hour = Math.floor(dayMins / 60); const minute = dayMins - hour * 60; const second = daySecs - dayMins * 60; const millis = unixMillis % 1000; return formatISO(year, month, day, hour, minute, second, millis); }; /** * Utility function to check if an object has own properties. * Assumes its input is an object in standard JavaScript. * * @param {Object} obj - The object to check * @returns {boolean} */ const isEmpty = (obj) => { for (let prop in obj) { if (obj.hasOwnProperty(prop)) { return false; } } return true; }; /** * Removes equal to null properties from given object. * * @param {Object} obj - The object to clean * @returns {Object} */ const cleanObject = (obj) => { let target = {}; for (let prop in obj) { if (obj.hasOwnProperty(prop) && obj[prop] != null) { target[prop] = obj[prop]; } } return target; }; /** * Given the tag configuration, * returns the value to be used as the time property of a MoEngage event. * * @param {Object} tagConfig - The tag configuration * @returns {*} */ const getEventTime = (tagConfig) => { if (tagConfig.eventTime) { return getEventData(tagConfig.eventTime); } return unixMillisToISO(getTimestampMillis()); }; /** * Utility function that creates an object according to Event Property Rules. * * @param {Object[]} configProps - The event property rules * @returns {Object} */ const getEventDataByKeys = (configProps) => { const props = {}; configProps.forEach((p) => { let eventProperty = getEventData(p.key); if (eventProperty !== undefined) { props[p.mappedKey || p.key] = eventProperty; } }); return props; }; const generateUserAttributesObject = (eventData, tagConfig) => { const attributes = { language: eventData.language, time_zone: eventData['x-sp-geo_timezone'], }; if (eventData['x-sp-geo_longitude'] && eventData['x-sp-geo_latitude']) { attributes.current_location = { longitude: eventData['x-sp-geo_longitude'], latitude: eventData['x-sp-geo_latitude'], }; } if (tagConfig.includeCommonUserProperties && eventData.user_data) { attributes.email = eventData.user_data.email_address; attributes.phone = eventData.user_data.phone_number; if (eventData.user_data.address) { const address = eventData.user_data.address.street + ' ' + eventData.user_data.address.city + ' ' + eventData.user_data.address.region; attributes.address = address; attributes.country = eventData.user_data.address.country; attributes.postal_code = eventData.user_data.address.postal_code; } } if (tagConfig.userMappingRules && tagConfig.userMappingRules.length > 0) { return mergeObjects([attributes, getEventDataByKeys(tagConfig.userMappingRules)]); } return attributes; }; const generateEventProperties = (eventData, tagConfig) => { const properties = {}; if (tagConfig.includeCommonEventProperties) { properties.page_location = eventData.page_location; properties.page_encoding = eventData.page_encoding; properties.page_referrer = eventData.page_referrer; properties.page_title = eventData.page_title; properties.screen_resolution = eventData.screen_resolution; properties.viewport_size = eventData.viewport_size; } if (tagConfig.eventMappingRules && tagConfig.eventMappingRules.length > 0) { return mergeObjects([properties, getEventDataByKeys(tagConfig.eventMappingRules)]); } return properties; }; const generateEventObject = (eventData, tagConfig) => { const properties = generateEventProperties(eventData, tagConfig); return { attributes: properties, }; }; const getCustomerID = (eventData, tagConfig) => { if (tagConfig.userIdentifierType === 'userIdentifierVariable') { return tagConfig.userID; } return getEventData(tagConfig.userID); }; const generateAdditionalKeyValue = (dataType, tagConfig) => { const dataToLoop = (dataType === 'event' ? tagConfig.additionalEventKeyValue : tagConfig.additionalUserKeyValue) || []; return makeTableMap(dataToLoop, 'key', 'value'); }; /** * Given the tag configuration and event data, * generates payload */ const generatePayload = (eventData, tagConfig) => { const userAttrs = generateUserAttributesObject(eventData, tagConfig); const eventObject = generateEventObject(eventData, tagConfig); parseCustomEventAndEntities( eventData, tagConfig, eventObject.attributes, userAttrs ); const additionalEventData = generateAdditionalKeyValue('event', tagConfig); const additionalUserData = generateAdditionalKeyValue('user', tagConfig); const requestPayload = { type: "transition", elements: [ { type: "event", customer_id: getCustomerID(eventData, tagConfig), actions: [ { action: tagConfig.eventNameOverride || eventData.event_name, current_time: getEventTime(tagConfig), attributes: flattenObject(mergeObjects([eventObject.attributes, additionalEventData])), } ] } ] }; if (!isEmpty(additionalUserData)) { requestPayload.elements.push( { type: "customer", customer_id: getCustomerID(eventData, tagConfig), attributes: flattenObject(mergeObjects([userAttrs, additionalUserData])) } ); } else if (!isEmpty(userAttrs)) { requestPayload.elements.push( { type: "customer", customer_id: getCustomerID(eventData, tagConfig), attributes: flattenObject(userAttrs) } ); } return cleanObject(requestPayload); }; // Generates options for api call const generateRequestOptions = (tagConfig, redact) => { const authToken = tagConfig.apiID + ':' + tagConfig.apiKey; const authTokenHashed = toBase64(authToken); return { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: redact ? 'redacted' : 'Basic ' + authTokenHashed, // Don't get confused here. Below is correct 'MOE-APPKEY': redact ? 'redacted' : tagConfig.apiID, }, }; }; // **************************** Helpers End **************************** // **************************** Main **************************** const url = 'https://api-0' + data.dataCenter + '.moengage.com/v1/transition/' + data.apiID; const eventData = getAllEventData(); const loggingEnabled = determineIsLoggingEnabled(data.logType); const traceIdHeader = loggingEnabled ? getRequestHeader('trace-id') : undefined; const stdLogInfo = { tagName: tagName, traceId: traceIdHeader, eventName: eventData.event_name, }; doLogging('Message', stdLogInfo, { msg: eventData }); const requestOptions = generateRequestOptions(data, false); const requestPayload = generatePayload(eventData, data); if (loggingEnabled) { const logRequestOptions = generateRequestOptions(data, true); doLogging('Request', stdLogInfo, { requestMethod: logRequestOptions.method, requestUrl: url, requestHeaders: logRequestOptions.headers, requestBody: requestPayload, }); } sendHttpRequest(url, requestOptions, JSON.stringify(requestPayload)) .then((response) => { if (loggingEnabled) { doLogging('Response', stdLogInfo, { responseStatusCode: response.statusCode, responseHeaders: response.headers, responseBody: response.body, }); } if (response.statusCode >= 200 && response.statusCode < 300) { data.gtmOnSuccess(); return; } data.gtmOnFailure(); }, (errorData) => { doLogging('Message', stdLogInfo, { msg: 'HTTP Request promise reject reason: ' + errorData.error }); }); ___SERVER_PERMISSIONS___ [ { "instance": { "key": { "publicId": "read_request", "versionId": "1" }, "param": [ { "key": "headerWhitelist", "value": { "type": 2, "listItem": [ { "type": 3, "mapKey": [ { "type": 1, "string": "headerName" } ], "mapValue": [ { "type": 1, "string": "trace-id" } ] } ] } }, { "key": "headersAllowed", "value": { "type": 8, "boolean": true } }, { "key": "requestAccess", "value": { "type": 1, "string": "specific" } }, { "key": "headerAccess", "value": { "type": 1, "string": "specific" } }, { "key": "queryParameterAccess", "value": { "type": 1, "string": "any" } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true }, { "instance": { "key": { "publicId": "logging", "versionId": "1" }, "param": [ { "key": "environments", "value": { "type": 1, "string": "all" } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true }, { "instance": { "key": { "publicId": "read_container_data", "versionId": "1" }, "param": [] }, "isRequired": true }, { "instance": { "key": { "publicId": "read_event_data", "versionId": "1" }, "param": [ { "key": "eventDataAccess", "value": { "type": 1, "string": "any" } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true }, { "instance": { "key": { "publicId": "send_http", "versionId": "1" }, "param": [ { "key": "allowedUrls", "value": { "type": 1, "string": "any" } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true } ] ___TESTS___ scenarios: - name: Test defaults code: | const mockData = { dataCenter: 2, apiID: 'testApiID', apiKey: 'testApiKey', userID: 'testUserID', userIdentifierType: 'userIdentifierVariable', includeSelfDescribingEvent: true, extractFromArray: true, includeEntities: 'all', includeCommonEventProperties: true, includeCommonUserProperties: true, includeAllAtomicProperties: true, logType: 'debug', }; const testEvent = mockEventObjectSelfDesc; // to assert on let argUrl, argOptions, argBody; // Mocks const httpPromiseResolve = promise.create((resolve, reject) => { const resolvedValue = { statusCode: 200, headers: { foo: 'bar' }, body: 'ok', }; resolve(resolvedValue); }); mock('sendHttpRequest', function () { argUrl = arguments[0]; argOptions = arguments[1]; argBody = arguments[2]; return httpPromiseResolve; }); mock('getContainerVersion', function () { let containerVersion = { debugMode: true, previewMode: false, }; return containerVersion; }); mock('getAllEventData', function () { return testEvent; }); mock('getEventData', function (x) { return getFromPath(x, testEvent); }); // Expectations const expectedUrl = 'https://api-0' + mockData.dataCenter + '.moengage.com/v1/transition/' + mockData.apiID; const expectedOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Basic ' + toB64(mockData.apiID + ':' + mockData.apiKey), 'MOE-APPKEY': mockData.apiID, }, }; const expectedBody = { "type": "transition", "elements": [ { "type": "event", "customer_id": "testUserID", "actions": [ { "action": "media_player_event", "current_time": "2022-09-05T18:39:56.056Z", "attributes": { "page_location": "http://localhost:8000/", "page_encoding": "windows-1252", "screen_resolution": "1920x1080", "viewport_size": "1044x975", "app_id": "media-test", "platform": "web", "dvce_created_tstamp": "1658567928426", "event_id": "c2084e30-5e4f-4d9c-86b2-e0bc3781509a", "name_tracker": "spTest", "v_tracker": "js-3.5.0", "domain_sessionid": "1ab28b79-bfdd-4855-9bf1-5199ce15beac", "domain_sessionidx": 1, "br_cookies": "1", "br_colordepth": "24", "br_viewwidth": 1044, "br_viewheight": 975, "dvce_screenwidth": 1920, "dvce_screenheight": 1080, "doc_charset": "windows-1252", "doc_width": 1044, "doc_height": 975, "dvce_sent_tstamp": "1658567928427", "type": "play", "osType": "myOsType", "osVersion": "myOsVersion", "deviceManufacturer": "myDevMan", "deviceModel": "myDevModel", "autoPlay": false, "avaliablePlaybackRates": [ 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ], "buffering": false, "controls": true, "cued": false, "loaded": 3, "playbackQuality": "medium", "playerId": "youtube-song", "unstarted": false, "url": "https://www.youtube.com/watch?v=foobarbaz", "avaliableQualityLevels": [ "hd1080", "hd720", "large", "medium", "small", "tiny", "auto" ], "currentTime": 0.015303093460083008, "duration": 190.301, "ended": false, "loop": false, "muted": false, "paused": false, "playbackRate": 1, "volume": 100, "id": "68027aa2-34b1-4018-95e3-7176c62dbc84", "email_address": "foo@test.io", "userId": "fd0e5288-e89b-45df-aad5-6d0c6eda6198", "sessionId": "1ab28b79-bfdd-4855-9bf1-5199ce15beac", "eventIndex": 24, "sessionIndex": 1, "previousSessionId": null, "storageMechanism": "COOKIE_1", "firstEventId": "40fbdb30-1b99-42a3-99f7-850dacf5be43", "firstEventTimestamp": "2022-07-23T09:08:04.451Z" } } ] }, { "type": "customer", "customer_id": "testUserID", "attributes": { "language": "en-US", "email": "foo@test.io" } } ] }; // Call runCode to run the template's code runCode(mockData); // Assertions assertApi('sendHttpRequest').wasCalled(); assertThat(argUrl).isStrictlyEqualTo(expectedUrl); assertThat(argOptions).isEqualTo(expectedOptions); const body = jsonApi.parse(argBody); assertThat(body).isEqualTo(expectedBody); assertApi('logToConsole').wasCalled(); - name: Test no atomic properties code: | const mockData = { dataCenter: 2, apiID: 'testApiID', apiKey: 'testApiKey', userID: 'testUserID', userIdentifierType: 'userIdentifierVariable', includeSelfDescribingEvent: true, includeEntities: 'all', includeCommonEventProperties: true, includeCommonUserProperties: true, includeAllAtomicProperties: false, logType: 'debug', }; const testEvent = mockEventObjectSelfDesc; // to assert on let argUrl, argOptions, argBody; // Mocks const httpPromiseResolve = promise.create((resolve, reject) => { const resolvedValue = { statusCode: 200, headers: { foo: 'bar' }, body: 'ok', }; resolve(resolvedValue); }); mock('sendHttpRequest', function () { argUrl = arguments[0]; argOptions = arguments[1]; argBody = arguments[2]; return httpPromiseResolve; }); mock('getContainerVersion', function () { let containerVersion = { debugMode: true, previewMode: false, }; return containerVersion; }); mock('getAllEventData', function () { return testEvent; }); mock('getEventData', function (x) { return getFromPath(x, testEvent); }); // Expectations const expectedUrl = 'https://api-0' + mockData.dataCenter + '.moengage.com/v1/transition/' + mockData.apiID; const expectedOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Basic ' + toB64(mockData.apiID + ':' + mockData.apiKey), 'MOE-APPKEY': mockData.apiID, }, }; const expectedBody = { type: 'transition', elements: [ { type: 'event', customer_id: 'testUserID', actions: [ { action: 'media_player_event', current_time: '2022-09-05T18:39:56.056Z', attributes: { page_location: 'http://localhost:8000/', page_encoding: 'windows-1252', screen_resolution: '1920x1080', viewport_size: '1044x975', type: 'play', osType: 'myOsType', osVersion: 'myOsVersion', deviceManufacturer: 'myDevMan', deviceModel: 'myDevModel', autoPlay: false, avaliablePlaybackRates: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], buffering: false, controls: true, cued: false, loaded: 3, playbackQuality: 'medium', playerId: 'youtube-song', unstarted: false, url: 'https://www.youtube.com/watch?v=foobarbaz', avaliableQualityLevels: [ 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'auto', ], currentTime: 0.015303093460083008, duration: 190.301, ended: false, loop: false, muted: false, paused: false, playbackRate: 1, volume: 100, id: '68027aa2-34b1-4018-95e3-7176c62dbc84', email_address: 'foo@test.io', userId: 'fd0e5288-e89b-45df-aad5-6d0c6eda6198', sessionId: '1ab28b79-bfdd-4855-9bf1-5199ce15beac', eventIndex: 24, sessionIndex: 1, previousSessionId: null, storageMechanism: 'COOKIE_1', firstEventId: '40fbdb30-1b99-42a3-99f7-850dacf5be43', firstEventTimestamp: '2022-07-23T09:08:04.451Z', }, }, ], }, { type: 'customer', customer_id: 'testUserID', attributes: { language: 'en-US', email: 'foo@test.io', }, }, ], }; // Call runCode to run the template's code runCode(mockData); // Assertions assertApi('sendHttpRequest').wasCalled(); assertThat(argUrl).isStrictlyEqualTo(expectedUrl); assertThat(argOptions).isEqualTo(expectedOptions); const body = jsonApi.parse(argBody); assertThat(body).isEqualTo(expectedBody); assertApi('logToConsole').wasCalled(); - name: Test with promise rejection code: | const mockData = { dataCenter: 2, apiID: 'testApiID', apiKey: 'testApiKey', userID: 'testUserID', includeSelfDescribingEvent: true, includeEntities: 'all', includeCommonEventProperties: true, includeCommonUserProperties: true, includeAllAtomicProperties: true, logType: 'debug', }; const testEvent = mockEventObjectPageView; // to assert on let argUrl, argOptions, argBody; // Mocks const httpPromiseReject = promise.create((resolve, reject) => { const rejectReason = { reason: 'timed_out', }; reject(rejectReason); }); mock('sendHttpRequest', function () { argUrl = arguments[0]; argOptions = arguments[1]; argBody = arguments[2]; return httpPromiseReject; }); mock('getContainerVersion', function () { let containerVersion = { debugMode: true, previewMode: true, }; return containerVersion; }); mock('getAllEventData', function () { return testEvent; }); mock('getEventData', function (x) { return getFromPath(x, testEvent); }); // Call runCode to run the template's code runCode(mockData); // Assertions assertApi('sendHttpRequest').wasCalled(); // Expectations assertApi('logToConsole').wasCalled(); // assertApi('logToConsole').wasCalledWith(); // TODO assertApi('logToConsole').wasCalled(); // assertApi('logToConsole').wasCalledWith(); // TODO assertApi('logToConsole').wasCalled(); // assertApi('logToConsole').wasCalledWith(); // TODO - name: Test include none code: | const mockData = { dataCenter: 2, apiID: 'testApiID', apiKey: 'testApiKey', userID: 'testUserID', userIdentifierType: 'userIdentifierVariable', includeSelfDescribingEvent: true, includeEntities: 'none', includeCommonEventProperties: true, includeCommonUserProperties: true, includeAllAtomicProperties: false, logType: 'debug', }; const testEvent = mockEventObjectPageView; // to assert on let argUrl, argOptions, argBody; // Mocks const httpPromiseResolve = promise.create((resolve, reject) => { const resolvedValue = { statusCode: 200, headers: { foo: 'bar' }, body: 'ok', }; resolve(resolvedValue); }); mock('sendHttpRequest', function () { argUrl = arguments[0]; argOptions = arguments[1]; argBody = arguments[2]; return httpPromiseResolve; }); mock('getContainerVersion', function () { let containerVersion = { debugMode: true, previewMode: false, }; return containerVersion; }); mock('getAllEventData', function () { return testEvent; }); mock('getEventData', function (x) { return getFromPath(x, testEvent); }); // Expectations const expectedUrl = 'https://api-0' + mockData.dataCenter + '.moengage.com/v1/transition/' + mockData.apiID; const expectedOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Basic ' + toB64(mockData.apiID + ':' + mockData.apiKey), 'MOE-APPKEY': mockData.apiID, }, }; const expectedBody = { type: 'transition', elements: [ { type: 'event', customer_id: mockData.userID, actions: [ { action: 'page_view', current_time: '2022-09-05T18:39:56.056Z', attributes: { page_location: testEvent.page_location, page_encoding: testEvent.page_encoding, page_referrer: testEvent.page_referrer, page_title: testEvent.page_title, screen_resolution: testEvent.screen_resolution, viewport_size: testEvent.viewport_size, // none of the Snowplow atomic props are included }, }, ], }, { type: 'customer', customer_id: mockData.userID, attributes: { language: testEvent.language, country: testEvent.user_data.address.country, email: testEvent.user_data.email_address, phone: testEvent.user_data.phone_number, postal_code: testEvent.user_data.address.postal_code, address: testEvent.user_data.address.street + ' ' + testEvent.user_data.address.city + ' ' + testEvent.user_data.address.region, }, }, ], }; // Call runCode to run the template's code runCode(mockData); // Assertions assertApi('sendHttpRequest').wasCalled(); assertThat(argUrl).isStrictlyEqualTo(expectedUrl); assertThat(argOptions).isEqualTo(expectedOptions); const body = jsonApi.parse(argBody); assertThat(body).isEqualTo(expectedBody); assertApi('logToConsole').wasCalled(); - name: Test rules and additional pairs code: | const mockData = { dataCenter: 3, apiID: 'testApiId', apiKey: 'testApiKey', userIdentifierType: 'userIdentifierEvent', userID: 'user_id', includeSelfDescribingEvent: true, includeAllAtomicProperties: true, includeEntities: 'all', entityMappingRules: [ { key: 'x-sp-contexts_com_snowplowanalytics_snowplow_mobile_context_1', mappedKey: 'mobile_ctx', // This does not take effect propertiesObjectToPopulate: 'event_object', version: 'control', }, { key: 'x-sp-contexts_com_google_tag-manager_server-side_user_data_1', mappedKey: 'udata_ctx', // This does not take effect propertiesObjectToPopulate: 'user_attributes_object', version: 'free', }, ], entityExclusionRules: [ { key: 'x-sp-contexts_com_snowplowanalytics_snowplow_web_page_1', version: 'control', }, ], includeCommonEventProperties: false, eventMappingRules: [ { key: 'x-sp-contexts_com_snowplowanalytics_snowplow_web_page_1.0.id', mappedKey: 'page_view_id', }, ], includeCommonUserProperties: true, userMappingRules: [ { key: 'x-sp-contexts_com_snowplowanalytics_snowplow_client_session_1.0.userId', mappedKey: 'client_session_id', }, ], additionalEventKeyValue: [ { key: 'additional_event_key', value: 'test_value' }, ], additionalUserKeyValue: [{ key: 'additional_user_key', value: 'test_value' }], logType: 'debug', }; const testEvent = mockEventObjectSelfDesc; // to assert on let argUrl, argOptions, argBody; // Mocks const httpPromiseResolve = promise.create((resolve, reject) => { const resolvedValue = { statusCode: 200, headers: { foo: 'bar' }, body: 'ok', }; resolve(resolvedValue); }); mock('sendHttpRequest', function () { argUrl = arguments[0]; argOptions = arguments[1]; argBody = arguments[2]; return httpPromiseResolve; }); mock('getContainerVersion', function () { let containerVersion = { debugMode: true, previewMode: false, }; return containerVersion; }); mock('getAllEventData', function () { return testEvent; }); mock('getEventData', function (x) { return getFromPath(x, testEvent); }); // Expectations const expectedUrl = 'https://api-0' + mockData.dataCenter + '.moengage.com/v1/transition/' + mockData.apiID; const expectedOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Basic ' + toB64(mockData.apiID + ':' + mockData.apiKey), 'MOE-APPKEY': mockData.apiID, }, }; const expectedBody = { type: 'transition', elements: [ { type: 'event', customer_id: testEvent.user_id, actions: [ { action: testEvent.event_name, current_time: '2022-09-05T18:39:56.056Z', attributes: { app_id: testEvent['x-sp-app_id'], platform: testEvent['x-sp-platform'], dvce_created_tstamp: testEvent['x-sp-dvce_created_tstamp'], event_id: testEvent['x-sp-event_id'], name_tracker: testEvent['x-sp-name_tracker'], v_tracker: testEvent['x-sp-v_tracker'], domain_sessionid: testEvent['x-sp-domain_sessionid'], domain_sessionidx: testEvent['x-sp-domain_sessionidx'], br_cookies: testEvent['x-sp-br_cookies'], br_colordepth: testEvent['x-sp-br_colordepth'], br_viewwidth: testEvent['x-sp-br_viewwidth'], br_viewheight: testEvent['x-sp-br_viewheight'], dvce_screenwidth: testEvent['x-sp-dvce_screenwidth'], dvce_screenheight: testEvent['x-sp-dvce_screenheight'], doc_charset: testEvent['x-sp-doc_charset'], doc_width: testEvent['x-sp-doc_width'], doc_height: testEvent['x-sp-doc_height'], dvce_sent_tstamp: testEvent['x-sp-dvce_sent_tstamp'], type: 'play', osType: 'myOsType', osVersion: 'myOsVersion', deviceManufacturer: 'myDevMan', deviceModel: 'myDevModel', autoPlay: false, avaliablePlaybackRates: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], buffering: false, controls: true, cued: false, loaded: 3, playbackQuality: 'medium', playerId: 'youtube-song', unstarted: false, url: 'https://www.youtube.com/watch?v=foobarbaz', avaliableQualityLevels: [ 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'auto', ], currentTime: 0.015303093460083008, duration: 190.301, ended: false, loop: false, muted: false, paused: false, playbackRate: 1, volume: 100, userId: 'fd0e5288-e89b-45df-aad5-6d0c6eda6198', sessionId: '1ab28b79-bfdd-4855-9bf1-5199ce15beac', eventIndex: 24, sessionIndex: 1, previousSessionId: null, storageMechanism: 'COOKIE_1', firstEventId: '40fbdb30-1b99-42a3-99f7-850dacf5be43', firstEventTimestamp: '2022-07-23T09:08:04.451Z', page_view_id: testEvent[ 'x-sp-contexts_com_snowplowanalytics_snowplow_web_page_1' ][0].id, additional_event_key: 'test_value', }, }, ], }, { type: 'customer', customer_id: testEvent.user_id, attributes: { language: 'en-US', email: 'foo@test.io', additional_user_key: 'test_value', client_session_id: testEvent[ 'x-sp-contexts_com_snowplowanalytics_snowplow_client_session_1' ][0].userId, email_address: 'foo@test.io', }, }, ], }; // Call runCode to run the template's code runCode(mockData); // Assertions assertApi('sendHttpRequest').wasCalled(); assertThat(argUrl).isStrictlyEqualTo(expectedUrl); assertThat(argOptions).isEqualTo(expectedOptions); const body = jsonApi.parse(argBody); assertThat(body).isEqualTo(expectedBody); assertApi('logToConsole').wasCalled(); setup: |- const jsonApi = require('JSON'); const getTypeOf = require('getType'); const logToConsole = require('logToConsole'); const promise = require('Promise'); const toB64 = require('toBase64'); const mockEventObjectPageView = { event_name: 'page_view', client_id: 'd54a1904-7798-401a-be0b-1a83bea73634', user_id: 'snow123', language: 'en-GB', page_encoding: 'UTF-8', page_hostname: 'snowplow.io', page_location: 'https://snowplow.io/', page_path: '/', page_referrer: 'referer', page_title: 'Collect, manage and operationalize behavioral data at scale | Snowplow', screen_resolution: '1920x1080', viewport_size: '745x1302', user_agent: 'user-agent', origin: 'origin', host: 'host', 'x-sp-geo_country': 'geo_country', // fake - added by hand to test 'x-sp-app_id': 'website', 'x-sp-platform': 'web', 'x-sp-dvce_created_tstamp': '1628586512246', 'x-sp-event_id': '8676de79-0eba-4435-ad95-8a41a8a0129c', 'x-sp-name_tracker': 'sp', 'x-sp-v_tracker': 'js-2.18.1', 'x-sp-domain_sessionid': 'e7580b71-227b-4868-9ea9-322a263ce885', 'x-sp-domain_sessionidx': 1, 'x-sp-br_cookies': '1', 'x-sp-br_colordepth': '24', 'x-sp-br_viewwidth': 745, 'x-sp-br_viewheight': 1302, 'x-sp-dvce_screenwidth': 1920, 'x-sp-dvce_screenheight': 1080, 'x-sp-doc_charset': 'UTF-8', 'x-sp-doc_width': 730, 'x-sp-doc_height': 12393, 'x-sp-dvce_sent_tstamp': '1628586512248', 'x-sp-contexts_com_snowplowanalytics_snowplow_web_page_1': [ { id: 'a86c42e5-b831-45c8-b706-e214c26b4b3d', }, ], 'x-sp-contexts_org_w3_performance_timing_1': [ { navigationStart: 1628586508610, unloadEventStart: 0, unloadEventEnd: 0, redirectStart: 0, redirectEnd: 0, fetchStart: 1628586508610, domainLookupStart: 1628586508637, domainLookupEnd: 1628586508691, connectStart: 1628586508691, connectEnd: 1628586508763, secureConnectionStart: 1628586508721, requestStart: 1628586508763, responseStart: 1628586508797, responseEnd: 1628586508821, domLoading: 1628586509076, domInteractive: 1628586509381, domContentLoadedEventStart: 1628586509408, domContentLoadedEventEnd: 1628586509417, domComplete: 1628586510332, loadEventStart: 1628586510332, loadEventEnd: 1628586510334, }, ], 'x-sp-contexts_com_google_tag-manager_server-side_user_data_1': [ { email_address: 'foo@example.com', phone_number: '+15551234567', address: { first_name: 'Jane', last_name: 'Doe', street: '123 Fake St', city: 'San Francisco', region: 'CA', postal_code: '94016', country: 'US', }, }, ], user_data: { email_address: 'foo@example.com', phone_number: '+15551234567', address: { first_name: 'Jane', last_name: 'Doe', street: '123 Fake St', city: 'San Francisco', region: 'CA', postal_code: '94016', country: 'US', }, }, ga_session_id: 'e7580b71-227b-4868-9ea9-322a263ce885', ga_session_number: '1', 'x-ga-mp2-seg': '1', 'x-ga-protocol_version': '2', 'x-ga-page_id': 'a86c42e5-b831-45c8-b706-e214c26b4b3d', ip_override: '1.2.3.4', }; const mockEventObjectSelfDesc = { event_name: 'media_player_event', client_id: 'fd0e5288-e89b-45df-aad5-6d0c6eda6198', language: 'en-US', page_encoding: 'windows-1252', page_hostname: 'localhost', page_location: 'http://localhost:8000/', page_path: '/', screen_resolution: '1920x1080', user_id: 'tester', viewport_size: '1044x975', user_agent: 'curl/7.81.0', host: 'host', 'x-sp-app_id': 'media-test', 'x-sp-platform': 'web', 'x-sp-dvce_created_tstamp': '1658567928426', 'x-sp-event_id': 'c2084e30-5e4f-4d9c-86b2-e0bc3781509a', 'x-sp-name_tracker': 'spTest', 'x-sp-v_tracker': 'js-3.5.0', 'x-sp-domain_sessionid': '1ab28b79-bfdd-4855-9bf1-5199ce15beac', 'x-sp-domain_sessionidx': 1, 'x-sp-br_cookies': '1', 'x-sp-br_colordepth': '24', 'x-sp-br_viewwidth': 1044, 'x-sp-br_viewheight': 975, 'x-sp-dvce_screenwidth': 1920, 'x-sp-dvce_screenheight': 1080, 'x-sp-doc_charset': 'windows-1252', 'x-sp-doc_width': 1044, 'x-sp-doc_height': 975, 'x-sp-dvce_sent_tstamp': '1658567928427', 'x-sp-self_describing_event_com_snowplowanalytics_snowplow_media_player_event_1': { type: 'play' }, 'x-sp-contexts_com_snowplowanalytics_snowplow_mobile_context_1': [ { osType: 'myOsType', osVersion: 'myOsVersion', deviceManufacturer: 'myDevMan', deviceModel: 'myDevModel', }, ], 'x-sp-contexts_com_youtube_youtube_1': [ { autoPlay: false, avaliablePlaybackRates: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], buffering: false, controls: true, cued: false, loaded: 3, playbackQuality: 'medium', playerId: 'youtube-song', unstarted: false, url: 'https://www.youtube.com/watch?v=foobarbaz', avaliableQualityLevels: [ 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'auto', ], }, ], 'x-sp-contexts_com_snowplowanalytics_snowplow_media_player_1': [ { currentTime: 0.015303093460083008, duration: 190.301, ended: false, loop: false, muted: false, paused: false, playbackRate: 1, volume: 100, }, ], 'x-sp-contexts_com_snowplowanalytics_snowplow_web_page_1': [ { id: '68027aa2-34b1-4018-95e3-7176c62dbc84' }, ], 'x-sp-contexts_com_google_tag-manager_server-side_user_data_1': [ { email_address: 'foo@test.io' }, ], 'x-sp-contexts_com_snowplowanalytics_snowplow_client_session_1': [ { userId: 'fd0e5288-e89b-45df-aad5-6d0c6eda6198', sessionId: '1ab28b79-bfdd-4855-9bf1-5199ce15beac', eventIndex: 24, sessionIndex: 1, previousSessionId: null, storageMechanism: 'COOKIE_1', firstEventId: '40fbdb30-1b99-42a3-99f7-850dacf5be43', firstEventTimestamp: '2022-07-23T09:08:04.451Z', }, ], 'x-sp-contexts': [ { schema: 'iglu:com.youtube/youtube/jsonschema/1-0-0', data: { autoPlay: false, avaliablePlaybackRates: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], buffering: false, controls: true, cued: false, loaded: 3, playbackQuality: 'medium', playerId: 'youtube-song', unstarted: false, url: 'https://www.youtube.com/watch?v=foobarbaz', avaliableQualityLevels: [ 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'auto', ], }, }, { schema: 'iglu:com.snowplowanalytics.snowplow/media_player/jsonschema/1-0-0', data: { currentTime: 0.015303093460083008, duration: 190.301, ended: false, loop: false, muted: false, paused: false, playbackRate: 1, volume: 100, }, }, { schema: 'iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0', data: { id: '68027aa2-34b1-4018-95e3-7176c62dbc84' }, }, { schema: 'iglu:com.google.tag-manager.server-side/user_data/jsonschema/1-0-0', data: { email_address: 'foo@test.io' }, }, { schema: 'iglu:com.snowplowanalytics.snowplow/client_session/jsonschema/1-0-2', data: { userId: 'fd0e5288-e89b-45df-aad5-6d0c6eda6198', sessionId: '1ab28b79-bfdd-4855-9bf1-5199ce15beac', eventIndex: 24, sessionIndex: 1, previousSessionId: null, storageMechanism: 'COOKIE_1', firstEventId: '40fbdb30-1b99-42a3-99f7-850dacf5be43', firstEventTimestamp: '2022-07-23T09:08:04.451Z', }, }, ], user_data: { email_address: 'foo@test.io' }, ga_session_id: '1ab28b79-bfdd-4855-9bf1-5199ce15beac', ga_session_number: '1', 'x-ga-mp2-seg': '1', 'x-ga-protocol_version': '2', 'x-ga-page_id': '68027aa2-34b1-4018-95e3-7176c62dbc84', }; // Helper for mocking const getFromPath = (path, obj) => { if (getTypeOf(path) === 'string' && getTypeOf(obj) === 'object') { const splitPath = path.split('.').filter((prop) => !!prop); return splitPath.reduce((acc, curr) => acc && acc[curr], obj); } return undefined; }; mock('getTimestampMillis', function () { return 1662403196056; // '2022-09-05T18:39:56.056Z' }); const testTime = '2022-09-05T18:39:56.056Z'; ___NOTES___ Created on 11/08/2023, 16:05:57