___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": "CLIENT", "id": "cvt_temp_public_id", "__wm": "VGVtcGxhdGUtQXV0aG9yX0FkdmFuY2VkVW5pdmVyc2FsQW5hbHl0aWNzLVNpbW8tQWhhdmE\u003d", "categories": [ "ANALYTICS" ], "version": 1, "securityGroups": [], "displayName": "Snowplow Client", "brand": { "id": "brand_dummy", "displayName": "", "thumbnail": "data:image/png;base64,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\u003d\u003d" }, "description": "Official client template for Snowplow. Use to serve any version of sp.js and forward events to a Snowplow collector via the Snowplow tag as well as forward to other GTM tags.", "containerContexts": [ "SERVER" ] } ___TEMPLATE_PARAMETERS___ [ { "type": "CHECKBOX", "name": "ipInclude", "checkboxText": "Forward Users IP address", "simpleValueType": true, "defaultValue": true, "alwaysInSummary": true }, { "type": "CHECKBOX", "name": "populateGaProps", "checkboxText": "Populate GAv4 Client Properties", "simpleValueType": true, "defaultValue": true, "alwaysInSummary": true, "help": "Populate the same \"x-ga-*\" properties which the GAv4 client populates to aid in compatibility for other Tags." }, { "type": "GROUP", "name": "spJsSettings", "displayName": "sp.js settings", "subParams": [ { "type": "CHECKBOX", "name": "serveSpJs", "checkboxText": "Serve sp.js from container", "simpleValueType": true, "defaultValue": true, "alwaysInSummary": true }, { "type": "TEXT", "name": "customSpJsName", "displayName": "sp.js name", "simpleValueType": true, "defaultValue": "sp.js", "valueValidators": [ { "type": "REGEX", "args": [ "[a-zA-Z0-9]*\\.js" ] } ], "alwaysInSummary": true, "help": "Use this setting to serve sp.js with an alternative filename. sp.js will continue to work." } ], "groupStyle": "ZIPPY_OPEN" }, { "type": "GROUP", "name": "additionalOptions", "displayName": "Additional Options", "groupStyle": "ZIPPY_CLOSED", "subParams": [ { "type": "TEXT", "name": "customPostPath", "displayName": "A Custom POST Path to accept requests on", "simpleValueType": true, "valueHint": "Set the preferred POST Path used in the Snowplow JavaScript Tracker. /com.snowplowanalytics.snowplow/tp2 will continue to work.", "help": "e.g. /com.mycompany/t", "valueValidators": [ { "type": "REGEX", "args": [ "\\/.*\\/.*" ] } ], "defaultValue": "/com.snowplowanalytics.snowplow/tp2" }, { "type": "CHECKBOX", "name": "claimGetRequests", "checkboxText": "Claim GET Requests", "simpleValueType": true, "defaultValue": true, "help": "Snowplow trackers send GET requests to /i, enable this to claim requests to this path using this client" }, { "type": "CHECKBOX", "name": "includeOriginalTp2Event", "checkboxText": "Include Original `tp2` Event", "simpleValueType": true, "defaultValue": true, "help": "Includes the original `tp2` request on the event. This ensures when using the Snowplow Tag the exact original request is replicated to the Snowplow Collector." }, { "type": "CHECKBOX", "name": "includeOriginalSelfDescribingEvent", "checkboxText": "Include Original Self Describing Event", "simpleValueType": true, "defaultValue": false, "help": "By default, the self describing event will be \"shredded\" into a key using the schema name as the key, this is a \"lossy\" transformation, as the Minor and Patch parts of the jsonschema version will be dropped. This flag populates the original, lossless, Self Describing Event as `x-sp-self_describing_event`." }, { "type": "CHECKBOX", "name": "includeOriginalContextsArray", "checkboxText": "Include Original Contexts Array", "simpleValueType": true, "help": "By default, the contexts will be \"shredded\" into separate keys using the context name as the key, this is a \"lossy\" transformation, as the Minor and Patch parts of the jsonschema version will be dropped. If you would like to keep the original \"lossless\" contexts array, enable this option.", "defaultValue": false } ] }, { "type": "GROUP", "name": "commonEventGroup", "displayName": "Advanced common event options", "groupStyle": "ZIPPY_CLOSED", "subParams": [ { "type": "GROUP", "name": "clientIdGroup", "displayName": "client_id", "groupStyle": "ZIPPY_OPEN", "subParams": [ { "type": "CHECKBOX", "name": "defaultClientId", "checkboxText": "Use default settings for client_id mapping in common event", "simpleValueType": true, "defaultValue": true, "help": "By default the Snowplow Client sets the \u003cstrong\u003eclient_id\u003c/strong\u003e as follows: If the event has the `client_session` context entity attached, its \u003cstrong\u003euserId\u003c/strong\u003e property is used. Else the \u003cstrong\u003edomain_userid\u003c/strong\u003e atomic property is used." }, { "type": "SIMPLE_TABLE", "name": "clientId", "displayName": "Specify client_id", "simpleTableColumns": [ { "defaultValue": "", "displayName": "Search Priority (higher value means higher priority)", "name": "priority", "type": "TEXT", "isUnique": true, "valueValidators": [ { "type": "NON_EMPTY" }, { "type": "NON_NEGATIVE_NUMBER" } ] }, { "defaultValue": "", "displayName": "Property name or path", "name": "propPath", "type": "TEXT", "isUnique": true, "valueValidators": [ { "type": "NON_EMPTY" } ] } ], "enablingConditions": [ { "paramName": "defaultClientId", "paramValue": false, "type": "EQUALS" } ], "help": "You can use this table to specify the rules to set the \u003cstrong\u003eclient_id\u003c/strong\u003e of the common event, which will override the default Snowplow Client behavior. For consistency downstream it is suggested to specify properties that apply to all Snowplow events (atomic or through global context entities). The \u003cstrong\u003eProperty name or path\u003c/strong\u003e column refers to the common event, so you can define alternative Snowplow properties using the \u003cstrong\u003ex-sp-\u003c/strong\u003e prefix before the enriched property name or nested path (using dot notation). Example values: `x-sp-network_userid` or `x-sp-contexts_com_acme_user_1.0.anonymous_identifier`.", "valueValidators": [] } ] }, { "type": "GROUP", "name": "userIdGroup", "displayName": "user_id", "groupStyle": "ZIPPY_OPEN", "subParams": [ { "type": "CHECKBOX", "name": "defaultUserId", "checkboxText": "Use default settings for user_id mapping in common event", "simpleValueType": true, "help": "By default the Snowplow Client sets the \u003cstrong\u003euser_id\u003c/strong\u003e from the \u003cstrong\u003euser_id\u003c/strong\u003e property of the Snowplow event.", "defaultValue": true }, { "type": "SIMPLE_TABLE", "name": "userId", "displayName": "Specify user_id", "simpleTableColumns": [ { "defaultValue": "", "displayName": "Search Priority (higher value means higher priority)", "name": "priority", "type": "TEXT", "isUnique": true, "valueValidators": [ { "type": "NON_EMPTY" }, { "type": "NON_NEGATIVE_NUMBER" } ] }, { "defaultValue": "", "displayName": "Property name or path", "name": "propPath", "type": "TEXT", "isUnique": true, "valueValidators": [ { "type": "NON_EMPTY" } ] } ], "enablingConditions": [ { "paramName": "defaultUserId", "paramValue": false, "type": "EQUALS" } ], "help": "You can use this table to specify the rules to set the \u003cstrong\u003euser_id\u003c/strong\u003e of the common event, which will override the default Snowplow Client behavior. For consistency downstream it is suggested to specify properties that apply to all Snowplow events (atomic or through global context entities). The \u003cstrong\u003eProperty name or path\u003c/strong\u003e column refers to the common event, so you can define alternative Snowplow properties using the \u003cstrong\u003ex-sp-\u003c/strong\u003e prefix before the enriched property name or nested path (using dot notation). For example: `x-sp-contexts_com_acme_user_entity_1.0.email`.", "valueValidators": [] } ] }, { "type": "GROUP", "name": "spContextGroup", "displayName": "Snowplow Entities mapping", "groupStyle": "ZIPPY_OPEN", "subParams": [ { "type": "CHECKBOX", "name": "mergeEntities", "checkboxText": "Merge selected Snowplow entities", "simpleValueType": true, "help": "Whether to allow merging of Snowplow context data to the Common Event.", "defaultValue": false }, { "type": "PARAM_TABLE", "name": "entityMergeRules", "displayName": "Context entities to merge", "paramTableColumns": [ { "param": { "type": "TEXT", "name": "schema", "displayName": "Schema", "simpleValueType": true, "help": "\u003cstrong\u003eRequired\u003c/strong\u003e: The schema of the context entity to merge.", "valueValidators": [ { "type": "NON_EMPTY" } ] }, "isUnique": true }, { "param": { "type": "SELECT", "name": "versionPolicy", "displayName": "Apply to all versions", "macrosInSelect": false, "selectItems": [ { "value": "control", "displayValue": "False" }, { "value": "free", "displayValue": "True" } ], "simpleValueType": true, "help": "Whether the rule applies to all versions of the context entity schema.", "valueValidators": [ { "type": "NON_EMPTY" } ], "defaultValue": "control" }, "isUnique": false }, { "param": { "type": "TEXT", "name": "prefix", "displayName": "Prefix", "simpleValueType": true, "help": "\u003cstrong\u003eOptional\u003c/strong\u003e: Specify a prefix to use for property names when merging.", "canBeEmptyString": true }, "isUnique": false }, { "param": { "type": "SELECT", "name": "mergeLevel", "displayName": "Merge to", "macrosInSelect": false, "selectItems": [ { "value": "rootLevel", "displayValue": "Event Properties" }, { "value": "customPath", "displayValue": "Custom" } ], "simpleValueType": true, "valueValidators": [ { "type": "NON_EMPTY" } ], "help": "Specify where to merge the context entity\u0027s properties.", "defaultValue": "rootLevel" }, "isUnique": false }, { "param": { "type": "TEXT", "name": "customPath", "displayName": "Custom path", "simpleValueType": true, "help": "\u003cstrong\u003eOptional\u003c/strong\u003e: Specify the custom path to merge the context entity data to. This option applies only if the \u003cstrong\u003eMerge to\u003c/strong\u003e column is set to \u003cstrong\u003eCustom\u003c/strong\u003e, else the row is considered invalid.", "canBeEmptyString": true }, "isUnique": false }, { "param": { "type": "SELECT", "name": "keepOriginal", "displayName": "Keep original mapping", "macrosInSelect": false, "selectItems": [ { "value": "keep", "displayValue": "Keep" }, { "value": "discard", "displayValue": "Discard" } ], "simpleValueType": true, "valueValidators": [ { "type": "NON_EMPTY" } ], "help": "Specify whether to keep the original mapping of the entity using its `x-sp-contexts_` prefixed name.", "defaultValue": "keep" }, "isUnique": false }, { "param": { "type": "TEXT", "name": "customTransformFun", "displayName": "Custom transformation", "simpleValueType": true, "help": "\u003cstrong\u003eOptional\u003c/strong\u003e: Specify a variable returning a function that represents a custom transformation of the context data array to the desired object before merging." }, "isUnique": false } ], "help": "Using this table you can specify the rules to merge Snowplow context entity data to the Common Event.", "enablingConditions": [ { "paramName": "mergeEntities", "paramValue": true, "type": "EQUALS" } ], "valueValidators": [ { "type": "NON_EMPTY" } ] } ] }, { "type": "GROUP", "name": "spSelfDescGroup", "displayName": "Snowplow Self-describing Event Mapping", "groupStyle": "ZIPPY_OPEN", "subParams": [ { "type": "CHECKBOX", "name": "mergeSelfDesc", "checkboxText": "Merge selected Snowplow self-describing event data", "simpleValueType": true, "help": "Whether to allow merging of Snowplow self-describing event data to the Common Event.", "defaultValue": false }, { "type": "PARAM_TABLE", "name": "selfDescMergeRules", "displayName": "Self-describing events to merge", "paramTableColumns": [ { "param": { "type": "TEXT", "name": "schema", "displayName": "Schema", "simpleValueType": true, "help": "\u003cstrong\u003eRequired\u003c/strong\u003e: The schema of the self-describing event to merge.", "valueValidators": [ { "type": "NON_EMPTY" } ] }, "isUnique": true }, { "param": { "type": "SELECT", "name": "versionPolicy", "displayName": "Apply to all versions", "macrosInSelect": false, "selectItems": [ { "value": "control", "displayValue": "False" }, { "value": "free", "displayValue": "True" } ], "simpleValueType": true, "help": "Whether the rule applies to all versions of the self-describing event.", "valueValidators": [ { "type": "NON_EMPTY" } ], "defaultValue": "control" }, "isUnique": false }, { "param": { "type": "TEXT", "name": "prefix", "displayName": "Prefix", "simpleValueType": true, "help": "\u003cstrong\u003eOptional\u003c/strong\u003e: Specify a prefix to use for property names when merging.", "canBeEmptyString": true }, "isUnique": false }, { "param": { "type": "SELECT", "name": "mergeLevel", "displayName": "Merge to", "macrosInSelect": false, "selectItems": [ { "value": "rootLevel", "displayValue": "Event Properties" }, { "value": "customPath", "displayValue": "Custom" } ], "simpleValueType": true, "valueValidators": [ { "type": "NON_EMPTY" } ], "help": "Specify where to merge the self-describing event properties.", "defaultValue": "rootLevel" }, "isUnique": false }, { "param": { "type": "TEXT", "name": "customPath", "displayName": "Custom path", "simpleValueType": true, "help": "\u003cstrong\u003eOptional\u003c/strong\u003e: Specify the custom path to merge the self-describing data to. This option applies only if the \u003cstrong\u003eMerge to\u003c/strong\u003e column is set to \u003cstrong\u003eCustom\u003c/strong\u003e, else the row is considered invalid.", "canBeEmptyString": true }, "isUnique": false }, { "param": { "type": "SELECT", "name": "keepOriginal", "displayName": "Keep original mapping", "macrosInSelect": false, "selectItems": [ { "value": "keep", "displayValue": "True" }, { "value": "discard", "displayValue": "False" } ], "simpleValueType": true, "valueValidators": [ { "type": "NON_EMPTY" } ], "help": "Specify whether to keep the original mapping of the self-describing event data using its `x-sp-self_describing_event_` prefixed name.", "defaultValue": "keep" }, "isUnique": false }, { "param": { "type": "TEXT", "name": "customTransformFun", "displayName": "Custom transformation", "simpleValueType": true, "help": "\u003cstrong\u003eOptional\u003c/strong\u003e: Specify a variable returning a function that represents a custom transformation of the self-describing data to the desired object before merging." }, "isUnique": false } ], "help": "Using this table you can specify the rules to merge Snowplow self-describing event data to the Common Event.", "enablingConditions": [ { "paramName": "mergeSelfDesc", "paramValue": true, "type": "EQUALS" } ], "valueValidators": [ { "type": "NON_EMPTY" } ] } ] } ] } ] ___SANDBOXED_JS_FOR_SERVER___ const claimRequest = require('claimRequest'); const createRegex = require('createRegex'); const getRequestPath = require('getRequestPath'); const log = require('logToConsole'); const sendHttpGet = require('sendHttpGet'); const returnResponse = require('returnResponse'); const runContainer = require('runContainer'); const setResponseBody = require('setResponseBody'); const setPixelResponse = require('setPixelResponse'); const getRequestQueryParameters = require('getRequestQueryParameters'); const setResponseStatus = require('setResponseStatus'); const setResponseHeader = require('setResponseHeader'); const getRequestMethod = require('getRequestMethod'); const getRequestBody = require('getRequestBody'); const getRequestHeader = require('getRequestHeader'); const getRemoteAddress = require('getRemoteAddress'); const templateDataStorage = require('templateDataStorage'); const parseUrl = require('parseUrl'); const JSON = require('JSON'); const getType = require('getType'); const fromBase64 = require('fromBase64'); const makeInteger = require('makeInteger'); const makeNumber = require('makeNumber'); const makeString = require('makeString'); const Math = require('Math'); const Object = require('Object'); const VERSION = '0.6.0'; const CDN = 'https://cdn.jsdelivr.net'; const REQUEST_PATH = getRequestPath(); const UA = getRequestHeader('user-agent'); const ORIGIN = getRequestHeader('origin'); const HOST = getRequestHeader('host'); const REFERER = getRequestHeader('referer'); const ANONYMOUS = getRequestHeader('SP-Anonymous'); const XSP_PREFIX = 'x-sp-'; const SELF_DESC_PREFIX = 'self_describing_event_'; const CONTEXTS_PREFIX = 'contexts_'; const XSP_SELF_DESC_PREFIX = XSP_PREFIX.concat(SELF_DESC_PREFIX); const XSP_CONTEXTS_PREFIX = XSP_PREFIX.concat(CONTEXTS_PREFIX); const DEF_COMMON = { clientId: [ {priority: 2,propPath:'x-sp-contexts_com_snowplowanalytics_snowplow_client_session_1.0.userId'}, {priority: 1,propPath: 'x-sp-domain_userid'}, ], userId: [ {priority: 1,propPath: 'x-sp-user_id'}, ]}; // The default transformation for context data const DEF_CTX_TRANSFORMATION = (ctxDataArray, event) => { // default transformation should not know // what to do with multi-entity context if (ctxDataArray.length !== 1) return undefined; return ctxDataArray[0]; }; // The default transformation for self-describing event data const DEF_SELF_DESC_TRANSFORMATION = (selfDescObj, event) => { return selfDescObj; }; /** * Returns whether a property name is a Snowplow context/entity property. * * @param {string} prop - The property name * @returns {boolean} */ const IS_CONTEXTS_PROP = (prop) => prop.indexOf(XSP_CONTEXTS_PREFIX) === 0; /** * Returns whether a property name is a Snowplow self-describing event property. * * @param {string} prop - The property name * @returns {boolean} */ const IS_SELF_DESC_PROP = (prop) => prop.indexOf(XSP_SELF_DESC_PREFIX) === 0; const CONFIG_MERGE_RULESET = { entity: { type: 'entity', flag: 'mergeEntities', configName: 'entityMergeRules', defaultTransformation: DEF_CTX_TRANSFORMATION, idCondition: IS_CONTEXTS_PROP, continueCondition: (x) => getType(x) !== 'array', }, selfDesc: { type: 'selfDesc', flag: 'mergeSelfDesc', configName: 'selfDescMergeRules', defaultTransformation: DEF_SELF_DESC_TRANSFORMATION, idCondition: IS_SELF_DESC_PROP, continueCondition: (x) => getType(x) !== 'object', }}; // Helpers /** * Decodes a base64url encoded string * Snowplow events are base64url encoded, so bare fromBase64 won't work. * * @param {string} str - The string to decode * @returns {string} The decoded string */ const base64urldecode = (str) => { const padding = 4 - (str.length % 4); switch (padding) { case 1: str += '='; break; case 2: str += '=='; break; } return fromBase64(str.replace('-', '+').replace('_', '/')); }; /** * Removes null or undefined toplevel properties from an object * * @param {Object} obj - The object to clean * @returns {Object} The resulting object */ const cleanObject = (obj) => { let target = {}; Object.keys(obj).forEach((k) => { if (obj[k] != null) target[k] = obj[k]; }); return target; }; /** * Does a clone of an object good enough for JSON Data Types * * @param {Object} obj - The object to clone * @returns {Object} The clone */ const clone = (obj) => { const objType = getType(obj); if (objType === 'array' || objType === 'object') { const result = objType === 'object' ? {} : []; Object.keys(obj).forEach((k) => {result[k] = clone(obj[k]);}); return result; } return obj; }; /** * Sends the Client response. Calls returnResponse API. * * @param {number} statusCode - The status code of the response * @param {string} [body] - The body of the response * @param {Object} [headers] - The headers of the response */ const sendResponse = (statusCode, body, headers) => { // Prevent CORS errors if (ORIGIN) { setResponseHeader('Access-Control-Allow-Origin', ORIGIN); setResponseHeader('Access-Control-Allow-Credentials', 'true'); setResponseHeader( 'Access-Control-Allow-Headers', 'Content-Type, SP-Anonymous' ); setResponseHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); } setResponseStatus(statusCode || 200); if (body) setResponseBody(body); if (headers) Object.keys(headers).forEach((k) => {setResponseHeader(k, headers[k]);}); returnResponse(); }; /** * Determines whether the provided string is uppercase. * * @param {string} value - The string to check * @returns {boolean} Whether the value is uppercase */ const isUpper = (value) => value === value.toUpperCase() && value !== value.toLowerCase(); /** * Converts a possibly CamelCase string to snake_case * * @param {string} value - The string to transform * @returns {string} The value in snake_case */ 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; }; /** * Parses a Snowplow schema to the expected major version format. * * @param {string} schema - The input schema * @returns {string} The expected/enriched major version format */ const parseSchemaToMajor = (schema) => { let fixed = schema.replace('iglu:', '').replace('jsonschema/', '').replace(createRegex('[./]', 'g'), '_'); for (let i = 0; i < 2; i++) { fixed = fixed.substring(0, fixed.lastIndexOf('-')); } return toSnakeCase(fixed).toLowerCase(); }; /** * Parses a Snowplow schema to the expected major version format, * also prefixed so as to match the contexts' output of the Snowplow Client. * * @param {string} schema - The input schema * @param {string} spType - 'entity' or 'selfDesc' * @returns {string} The expected output client event property */ const parseSchemaToMajorKeyValue = (schema, spType) => { const fullPrefix = spType === 'entity' ? XSP_CONTEXTS_PREFIX : XSP_SELF_DESC_PREFIX; const prefix = spType === 'entity' ? CONTEXTS_PREFIX : SELF_DESC_PREFIX; if (schema.indexOf(fullPrefix) === 0) return schema; if (schema.indexOf(prefix) === 0) return XSP_PREFIX + schema; if (schema.indexOf('iglu:') === 0) return fullPrefix + parseSchemaToMajor(schema); return schema; }; const getSelfDescribing = (event) => { let selfDescribing; if (event.ue_px) { const decoded = base64urldecode(event.ue_px); if (decoded) selfDescribing = JSON.parse(decoded); } else if (event.ue_pr) { selfDescribing = JSON.parse(event.ue_pr); } return selfDescribing ? selfDescribing.data : undefined; }; const getEventNameFromSchema = (event) => { const selfDescribing = getSelfDescribing(event); // Try to find event name from schema if (selfDescribing) { const schemaParts = selfDescribing.schema.split('/'); if (schemaParts.length > 1) return schemaParts[1]; } }; const getContexts = (event) => { let contexts; if (event.cx) { const decoded = base64urldecode(event.cx); if (decoded) contexts = JSON.parse(decoded); } else if (event.co) { contexts = JSON.parse(event.co); } return contexts ? contexts.data : undefined; }; const splitResolution = (resolution) => { const split_res = resolution ? resolution.split('x') : undefined; if (split_res && split_res.length === 2) return {width: makeInteger(split_res[0]),height: makeInteger(split_res[1])}; return { width: undefined, height: undefined }; }; const getEventName = (event) => { switch (event.e) { case 'pv': return 'page_view'; case 'pp': return 'page_ping'; case 'tr': return 'transaction'; case 'ti': return 'transaction_item'; case 'se': return event.se_ac; case 'ue': return getEventNameFromSchema(event); default: return event.e; } }; const payloadToSnowplowEvents = (payload) => { const events = JSON.parse(payload); if (getType(events) === 'object' && getType(events.data) === 'array') return events.data; return []; }; const enrichedPayloadToSnowplowEvents = (payload) => { const events = JSON.parse(payload); const eventType = getType(events); if (eventType === 'array') return events; if (eventType === 'object') return [events]; return []; }; /* * Gets the value in obj from path. * Path must be a string denoting a (nested) property path separated by '.' * e.g. getFromPath('a.b', {a: {b: 2}}) => 2 * * @param path {string} - the string to replace into * @param obj {Object} - the object to look into * @returns - the corresponding value or undefined */ const getFromPath = (path, obj) => { if (getType(path) === 'string') return path.split('.').filter((p) => !!p).reduce((a, c) => a && a[c], obj); }; /* * Helper function to locate properties from a given table (locator). * A locator is a table (array of objects) like the `clientId` and `userId` * tables, which are client configuration fields. * Each of its rows contains a priority and a *common event* path to look for. */ const locate = (locator, obj, postFunction) => { const ordLoc = locator.sort((x, y) => makeInteger(x.priority) > makeInteger(y.priority) ? -1 : 1); for (let i = 0; i < ordLoc.length; i++) { const located = getFromPath(ordLoc[i].propPath, obj); if (located !== undefined) { if (getType(postFunction) === 'function') return postFunction(located); return located; } } }; /* * Helper to ensure an array is returned even if template table has no rows. * Aids in Advanced Common Event Settings, where not supplying rows * e.g. for user_id table means "do not set it". */ const asArray = (x) => { if (getType(x) !== 'array') return []; return x; }; /** * Returns whether a string can be parsed as an integer. * * @param {string} x - The string to check * @returns {boolean} */ const isInt = (x) => { const y = Math.floor(x); if (y === 0) return true; return !!y; }; /** * Splits a string as a path by dot notation. * (used by both getFromPath and setFromPath) * * @param {string} stringPath - The string to split * @returns {string[]} The array of path components */ const splitStringPath = (stringPath) => stringPath.split('.').filter((p) => !!p); /** * Sets the value in obj from path (side-effects). * Overwrites if encounters existing properties, and creates nesting if needed. * @example * // returns {a: {b: {c: 3}}} * setFromPath('a.b.c', 3, {a: {b: 0}}) * @example * // returns {a: [{x: 4}]} * setFromPath('a.0.x', 4, {a: {b: 0}}) * @example * // returns {a: [4]} * setFromPath('a.0', 4, {a: {b: 0}}) * @example * // returns {a: [1,1,5]} * setFromPath('a.2', 5, {a: [1,1,1]}) * * @param {(string|string[])} path - The path where to set the value * @param {*} val - The value to be set * @param {Object} obj - The object to mutate * @param {Object} [target] - The object that the path refers to * @returns {Object} The object mutated with the value set */ const setFromPath = (path, val, obj, target) => { const numAsIdx = true; if (!target) target = obj; if (getType(path) === 'string') path = splitStringPath(path); if (path.length === 1) { target[path[0]] = val; return obj; } else if (path.length > 1) { const currKey = path[0]; const currType = getType(target[currKey]); const nextKey = path[1]; const isNextNum = isInt(nextKey); if ((!isNextNum && currType !== 'object') || (isNextNum && !numAsIdx && currType !== 'object')) target[currKey] = {}; else if (isNextNum && numAsIdx && currType !== 'array') target[currKey] = []; return setFromPath(path.slice(1), val, obj, target[currKey]); } return obj; }; /** * Sets a property of an object to a value. (side effects) * It is essentially a wrapper around setFromPath to enable * the "nest under" feature of the tag configuration. * * @param {string} prop - The property name to add * @param {*} setVal - The value to be set * @param {string} nest - The path to nest the property to * @param {Object} obj - The object to add the property to * @returns {Object} The object with the property added */ const addProperty = (prop, setVal, nest, obj) => { if (nest && getType(nest) === 'string') { const valType = getType(getFromPath(nest, obj)); if (['object', 'array'].indexOf(valType) < 0) setFromPath(nest, {}, obj); setFromPath(prop, setVal, getFromPath(nest, obj)); } else { setFromPath(prop, setVal, obj); } return obj; }; /* * Creates an object, which has common event properties as properties. * (currently supported: clientId, userId) * Used to enable advanced common event settings. */ const mkCommonProps = (xSpEvent, config) => { const locations = { clientId: config.defaultClientId ? DEF_COMMON.clientId : asArray(config.clientId), userId: config.defaultUserId ? DEF_COMMON.userId : asArray(config.userId), }; return cleanObject({ clientId: locate(locations.clientId, xSpEvent, makeString), userId: locate(locations.userId, xSpEvent, makeString), }); }; /** * 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) => schemaRef.replace(createRegex('_[0-9]+$'), ''); /** * Filters out invalid rules to avoid unintended behavior. * (e.g. version control being ignored if version num is not included in name) * * @param {Object[]} rules - The provided rules * @returns {Object[]} The valid rules */ const cleanRules = (rules) => { const lastNumRexp = createRegex('[0-9]$'); return rules.filter((row) => { const valid = {versionLogic: true,customLogic: true,functionLogic: true}; if (row.versionPolicy === 'control') valid.versionLogic = !!row.schema.match(lastNumRexp); if (row.mergeLevel === 'customPath') valid.customLogic = row.customPath !== ''; if (row.customTransformFun) valid.functionLogic = getType(row.customTransformFun) === 'function'; return valid.versionLogic && valid.customLogic && valid.functionLogic; }); }; /** * Parses the merge rules from the client configuration. * * @param {Object} config - The Client configuration * @returns {Object[]} */ const parseMergeRules = (config, ruleSet) => { if (!ruleSet) return []; const rules = config[ruleSet.configName]; if (rules) { const parsedRules = cleanRules(rules).map((row) => { const schema = parseSchemaToMajorKeyValue(row.schema, ruleSet.type); return { ref: row.versionPolicy === 'control' ? schema : mkVersionFree(schema), schema: schema, prefix: row.prefix || '', versionPolicy: row.versionPolicy, mergeLevel: row.mergeLevel, customPath: row.customPath || '', keepOriginal: row.keepOriginal, transformFun: row.customTransformFun || ruleSet.defaultTransformation, }; }); return parsedRules; } 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 {number} The index for the entity in the array */ const getReferenceIdx = (entity, refsList) => { for (let i = 0; i < refsList.length; i++) { const okControl = entity.indexOf(refsList[i]) === 0; const okFree = mkVersionFree(entity) === mkVersionFree(refsList[i]); if (okControl && okFree) return i; } return -1; }; /** * Merges context entity data to target object according to rule. * * @param {Object} target - The target object * @param {string} prop - The original property name * @param {Object} dataParam - The ctx or self-desc data of interest * @param {Object} rule - The rule to apply for merging * @returns {Object} The target modified after merging is applied */ const applyMergeRule = (target, prop, dataParam, rule) => { // we pass a clone of target to ensure no side effects to it const transformed = rule.transformFun(dataParam, clone(target)); if (getType(transformed) !== 'object') return target; // do not proceed Object.keys(transformed).forEach((prop) => { const name = rule.prefix ? rule.prefix.concat(prop) : prop; switch (rule.mergeLevel) { case 'customPath': addProperty(name, transformed[prop], rule.customPath, target); break; case 'rootLevel': target[name] = transformed[prop]; break; default: } }); if (rule.keepOriginal === 'discard') target[prop] = undefined; return target; }; /** * Applies merge rules * * @param {Object} original - The original common event object * @param {Object} target - The target object to be modified * @param {Object} config - The client configuration * @param {string} rulesKey - A string signifying which rule to apply * @returns {Object} The target modified after merging is applied */ const applyRules = (original, target, config, rulesKey) => { const ruleSet = CONFIG_MERGE_RULESET[rulesKey]; if (!ruleSet) return target; if (config[ruleSet.flag] !== true) return target; const mergeRules = parseMergeRules(config, ruleSet); const finalRefs = mergeRules.map((r) => r.ref); Object.entries(original).forEach((pair) => { if (ruleSet.idCondition(pair[0]) && !ruleSet.continueCondition(pair[1])) { const refIdx = getReferenceIdx(pair[0], finalRefs); if (refIdx >= 0) { const rule = mergeRules[refIdx]; applyMergeRule(target, pair[0], pair[1], rule); } } }); return target; }; /** * Higher order function used to derive functions to apply. * * @param {Object} original - The original common event contructed so far * @param {Object} cfg - The Client configuration * @returns {Function} A higher order function */ function withRulesEnv(original, cfg) { return function (fun, ruleSet) { return function (target) { return fun(original, target, cfg, ruleSet); }; }; } /** * Does the final postprocessing step of the common event. At this stage * the commonEvent is exactly at the format the Tags expect. * This means that here we can reuse Tags logic. * postprocess currently deals with: * - client_id and user_id based on Advanced common event options * - Snowplow Context Entities mapping/merging * (merge rules should not apply to multi-entity contexts) * * @param {Object} commonEvent - The common event contructed so far * @param {Object} event - An incoming event payload * @param {Object} config - The Client configuration * @returns {Object} The final common event */ const postprocess = (commonEvent, event, config) => { const fromAdvancedCommon = mkCommonProps(commonEvent, config); commonEvent.client_id = fromAdvancedCommon.clientId; commonEvent.user_id = fromAdvancedCommon.userId; if (!config.mergeEntities && !config.mergeSelfDesc) return commonEvent; const withRules = withRulesEnv(commonEvent, config); const final = [ withRules(applyRules, 'entity'), withRules(applyRules, 'selfDesc'), ].reduce((acc, curr) => curr(acc), clone(commonEvent)); return final; }; const populateAdditionalProperties = (commonEvent, event, config) => { // user_data if (commonEvent['x-sp-contexts_com_google_tag-manager_server-side_user_data_1']) { const userData = commonEvent['x-sp-contexts_com_google_tag-manager_server-side_user_data_1'][0]; commonEvent.user_data = cleanObject({ email_address: userData.email_address, phone_number: userData.phone_number, }); if (userData.address) { commonEvent.user_data.address = cleanObject({ first_name: userData.address.first_name, last_name: userData.address.last_name, street: userData.address.street, city: userData.address.city, region: userData.address.region, postal_code: userData.address.postal_code, country: userData.address.country, }); } } // GA props let sessionId = commonEvent['x-sp-domain_sessionid']; let sessionIndex = commonEvent['x-sp-domain_sessionidx']; if (commonEvent['x-sp-contexts_com_snowplowanalytics_snowplow_client_session_1']) { const mobileSessionData = commonEvent['x-sp-contexts_com_snowplowanalytics_snowplow_client_session_1'][0]; sessionId = mobileSessionData.sessionId; sessionIndex = mobileSessionData.sessionIndex; } if (config.populateGaProps) { commonEvent.ga_session_id = sessionId; commonEvent.ga_session_number = sessionIndex ? makeString(sessionIndex) : undefined; commonEvent['x-ga-mp2-seg'] = '1'; commonEvent['x-ga-protocol_version'] = '2'; if (commonEvent['x-sp-contexts_com_snowplowanalytics_snowplow_web_page_1']) { commonEvent['x-ga-page_id'] = commonEvent['x-sp-contexts_com_snowplowanalytics_snowplow_web_page_1'][0].id; } } return commonEvent; }; const addSelfDescPropsEnriched = (commonEvent, event, config) => { Object.keys(event).forEach((prop) => { if (prop.indexOf('unstruct_event') === 0) commonEvent[XSP_SELF_DESC_PREFIX + prop.replace('unstruct_event_', '')] = event[prop]; if (prop.indexOf('contexts_') === 0) commonEvent[XSP_PREFIX + prop] = event[prop]; }); return commonEvent; }; const addSelfDescPropsTp2 = (commonEvent, event, config) => { const selfDescribing = getSelfDescribing(event); if (selfDescribing) { commonEvent[XSP_SELF_DESC_PREFIX + parseSchemaToMajor(selfDescribing.schema)] = selfDescribing.data; if (config.includeOriginalSelfDescribingEvent) commonEvent['x-sp-self_describing_event'] = selfDescribing; } const contexts = getContexts(event); if (contexts) { contexts.forEach((c) => { const schemaKey = parseSchemaToMajor(c.schema); if (commonEvent[XSP_CONTEXTS_PREFIX + schemaKey]) commonEvent[XSP_CONTEXTS_PREFIX + schemaKey].push(c.data); else commonEvent[XSP_CONTEXTS_PREFIX + schemaKey] = [c.data]; }); if (config.includeOriginalContextsArray) commonEvent['x-sp-contexts'] = contexts; } return commonEvent; }; /** * Higher order function used to derive functions to apply. * * @param {Object} ev - The original event received by the Client * @param {Object} cfg - The Client configuration * @returns {Function} A higher order function */ function withEnv(ev, cfg) { return function (fun) { return function (commonEv) { return fun(commonEv, ev, cfg); }; }; } const mapSnowplowEnrichedEventToTagEvent = (event, config) => { const urlObject = parseUrl(event.page_url); let commonEvent = { event_name: event.event_name, language: event.br_lang, page_encoding: event.doc_charset, page_hostname: urlObject ? urlObject.hostname : undefined, page_location: event.page_url, page_path: urlObject ? urlObject.pathname : undefined, page_referrer: event.page_referrer ? event.page_referrer : REFERER, page_title: event.page_title, screen_resolution: event.dvce_screenwidth ? event.dvce_screenwidth + 'x' + event.dvce_screenheight : undefined, viewport_size: event.br_viewwidth ? event.br_viewwidth + 'x' + event.br_viewheight : undefined, user_agent: event.useragent, origin: ORIGIN, host: HOST, ip_override: config.ipInclude && !ANONYMOUS ? event.user_ipaddress : undefined, 'x-sp-anonymous': ANONYMOUS, 'x-sp-app_id': event.app_id, 'x-sp-platform': event.platform, 'x-sp-etl_tstamp': event.etl_tstamp, 'x-sp-collector_tstamp': event.collector_tstamp, 'x-sp-dvce_created_tstamp': event.dvce_created_tstamp, 'x-sp-event': event.event, 'x-sp-event_id': event.event_id, 'x-sp-txn_id': event.txn_id, 'x-sp-name_tracker': event.name_tracker, 'x-sp-v_tracker': event.v_tracker, 'x-sp-v_collector': event.v_collector, 'x-sp-v_etl': event.v_etl, 'x-sp-user_id': event.user_id, 'x-sp-user_fingerprint': event.user_fingerprint, 'x-sp-domain_userid': event.domain_userid, 'x-sp-domain_sessionidx': event.domain_sessionidx, 'x-sp-network_userid': event.network_userid, 'x-sp-geo_country': event.geo_country, 'x-sp-geo_region': event.geo_region, 'x-sp-geo_city': event.geo_city, 'x-sp-geo_zipcode': event.geo_zipcode, 'x-sp-geo_latitude': event.geo_latitude, 'x-sp-geo_longitude': event.geo_longitude, 'x-sp-geo_location': event.geo_location, 'x-sp-geo_region_name': event.geo_region_name, 'x-sp-ip_isp': event.ip_isp, 'x-sp-ip_organization': event.ip_organization, 'x-sp-ip_domain': event.ip_domain, 'x-sp-ip_netspeed': event.ip_netspeed, 'x-sp-page_urlscheme': event.page_urlscheme, 'x-sp-page_urlhost': event.page_urlhost, 'x-sp-page_urlport': event.page_urlport, 'x-sp-page_urlpath': event.page_urlpath, 'x-sp-page_urlquery': event.page_urlquery, 'x-sp-page_urlfragment': event.page_urlfragment, 'x-sp-refr_urlscheme': event.refr_urlscheme, 'x-sp-refr_urlhost': event.refr_urlhost, 'x-sp-refr_urlport': event.refr_urlport, 'x-sp-refr_urlpath': event.refr_urlpath, 'x-sp-refr_urlquery': event.refr_urlquery, 'x-sp-refr_urlfragment': event.refr_urlfragment, 'x-sp-refr_medium': event.refr_medium, 'x-sp-refr_source': event.refr_source, 'x-sp-refr_term': event.refr_term, 'x-sp-mkt_medium': event.mkt_medium, 'x-sp-mkt_source': event.mkt_source, 'x-sp-mkt_term': event.mkt_term, 'x-sp-mkt_content': event.mkt_content, 'x-sp-mkt_campaign': event.mkt_campaign, 'x-sp-se_category': event.se_category, 'x-sp-se_action': event.se_action, 'x-sp-se_label': event.se_label, 'x-sp-se_property': event.se_property, 'x-sp-se_value': event.se_value, 'x-sp-tr_orderid': event.tr_orderid, 'x-sp-tr_affiliation': event.tr_affiliation, 'x-sp-tr_total': event.tr_total, 'x-sp-tr_tax': event.tr_tax, 'x-sp-tr_shipping': event.tr_shipping, 'x-sp-tr_city': event.tr_city, 'x-sp-tr_state': event.tr_state, 'x-sp-tr_country': event.tr_country, 'x-sp-ti_orderid': event.ti_orderid, 'x-sp-ti_sku': event.ti_sku, 'x-sp-ti_name': event.ti_name, 'x-sp-ti_category': event.ti_category, 'x-sp-ti_price': event.ti_price, 'x-sp-ti_quantity': event.ti_quantity, 'x-sp-pp_xoffset_min': event.pp_xoffset_min, 'x-sp-pp_xoffset_max': event.pp_xoffset_max, 'x-sp-pp_yoffset_min': event.pp_yoffset_min, 'x-sp-pp_yoffset_max': event.pp_yoffset_max, 'x-sp-br_name': event.br_name, 'x-sp-br_family': event.br_family, 'x-sp-br_version': event.br_version, 'x-sp-br_type': event.br_type, 'x-sp-br_renderengine': event.br_renderengine, 'x-sp-br_features_pdf': event.br_features_pdf, 'x-sp-br_features_flash': event.br_features_flash, 'x-sp-br_features_java': event.br_features_java, 'x-sp-br_features_director': event.br_features_director, 'x-sp-br_features_quicktime': event.br_features_quicktime, 'x-sp-br_features_realplayer': event.br_features_realplayer, 'x-sp-br_features_windowsmedia': event.br_features_windowsmedia, 'x-sp-br_features_gears': event.br_features_gears, 'x-sp-br_features_silverlight': event.br_features_silverlight, 'x-sp-br_cookies': event.br_cookies, 'x-sp-br_colordepth': event.br_colordepth, 'x-sp-br_viewwidth': event.br_viewwidth, 'x-sp-br_viewheight': event.br_viewheight, 'x-sp-os_name': event.os_name, 'x-sp-os_family': event.os_family, 'x-sp-os_manufacturer': event.os_manufacturer, 'x-sp-os_timezone': event.os_timezone, 'x-sp-dvce_type': event.dvce_type, 'x-sp-dvce_ismobile': event.dvce_ismobile, 'x-sp-dvce_screenwidth': event.dvce_screenwidth, 'x-sp-dvce_screenheight': event.dvce_screenheight, 'x-sp-doc_width': event.doc_width, 'x-sp-doc_height': event.doc_height, 'x-sp-tr_currency': event.tr_currency, 'x-sp-tr_total_base': event.tr_total_base, 'x-sp-tr_tax_base': event.tr_tax_base, 'x-sp-tr_shipping_base': event.tr_shipping_base, 'x-sp-ti_currency': event.ti_currency, 'x-sp-ti_price_base': event.ti_price_base, 'x-sp-base_currency': event.base_currency, 'x-sp-geo_timezone': event.geo_timezone, 'x-sp-mkt_clickid': event.mkt_clickid, 'x-sp-mkt_network': event.mkt_network, 'x-sp-etl_tags': event.etl_tags, 'x-sp-dvce_sent_tstamp': event.dvce_sent_tstamp, 'x-sp-refr_domain_userid': event.refr_domain_userid, 'x-sp-refr_device_tstamp': event.refr_device_tstamp, 'x-sp-domain_sessionid': event.domain_sessionid, 'x-sp-derived_tstamp': event.derived_tstamp, 'x-sp-event_vendor': event.event_vendor, 'x-sp-event_name': event.event_name, 'x-sp-event_format': event.event_format, 'x-sp-event_version': event.event_version, 'x-sp-event_fingerprint': event.event_fingerprint, 'x-sp-true_tstamp': event.true_tstamp, }; const withCurrentEnv = withEnv(event, config); const result = [ withCurrentEnv(addSelfDescPropsEnriched), withCurrentEnv(populateAdditionalProperties), withCurrentEnv(postprocess), cleanObject, ].reduce((acc, curr) => curr(acc), commonEvent); return result; }; const mapSnowplowTp2EventToTagEvent = (event, config) => { const urlObject = parseUrl(event.url); const resolution = splitResolution(event.res); const viewport = splitResolution(event.vp); const doc = splitResolution(event.ds); let commonEvent = { event_name: getEventName(event), language: event.lang, page_encoding: event.cs, page_hostname: urlObject ? urlObject.hostname : undefined, page_location: event.url, page_path: urlObject ? urlObject.pathname : undefined, page_referrer: event.refr ? event.refr : REFERER, page_title: event.page, screen_resolution: event.res, viewport_size: event.vp, user_agent: UA, origin: ORIGIN, host: HOST, ip_override: config.ipInclude && !ANONYMOUS ? getRemoteAddress() : undefined, 'x-sp-anonymous': ANONYMOUS, 'x-sp-app_id': event.aid, 'x-sp-platform': event.p, 'x-sp-dvce_created_tstamp': event.dtm, 'x-sp-event_id': event.eid, 'x-sp-name_tracker': event.tna, 'x-sp-v_tracker': event.tv, 'x-sp-domain_sessionid': event.sid, 'x-sp-domain_sessionidx': event.vid ? makeInteger(event.vid) : undefined, 'x-sp-domain_userid': event.duid, 'x-sp-user_id': event.uid, 'x-sp-network_userid': event.nuid, 'x-sp-se_category': event.se_ca, 'x-sp-se_action': event.se_ac, 'x-sp-se_label': event.se_la, 'x-sp-se_property': event.se_pr, 'x-sp-se_value': event.se_va, 'x-sp-tr_orderid': event.tr_id, 'x-sp-tr_affiliation': event.tr_af, 'x-sp-tr_total': event.tr_tt ? makeNumber(event.tr_tt) : undefined, 'x-sp-tr_tax': event.tr_tx ? makeNumber(event.tr_tx) : undefined, 'x-sp-tr_shipping': event.tr_sh ? makeNumber(event.tr_sh) : undefined, 'x-sp-tr_city': event.tr_ci, 'x-sp-tr_state': event.tr_st, 'x-sp-tr_country': event.tr_co, 'x-sp-ti_orderid': event.ti_id, 'x-sp-ti_sku': event.ti_sk, 'x-sp-ti_name': event.ti_nm, 'x-sp-ti_category': event.ti_ca, 'x-sp-ti_price': event.ti_pr ? makeNumber(event.ti_pr) : undefined, 'x-sp-ti_quantity': event.ti_qu ? makeInteger(event.ti_qu) : undefined, 'x-sp-pp_xoffset_min': event.pp_mix ? makeInteger(event.pp_mix) : undefined, 'x-sp-pp_xoffset_max': event.pp_max ? makeInteger(event.pp_max) : undefined, 'x-sp-pp_yoffset_min': event.pp_miy ? makeInteger(event.pp_miy) : undefined, 'x-sp-pp_yoffset_max': event.pp_may ? makeInteger(event.pp_may) : undefined, 'x-sp-br_cookies': event.cookie, 'x-sp-br_colordepth': event.cd, 'x-sp-br_viewwidth': viewport.width, //event.vp 'x-sp-br_viewheight': viewport.height, 'x-sp-dvce_screenwidth': resolution.width, //event.res 'x-sp-dvce_screenheight': resolution.height, 'x-sp-doc_charset': event.cs, 'x-sp-doc_width': doc.width, //event.ds 'x-sp-doc_height': doc.height, 'x-sp-tr_currency': event.tr_cu, 'x-sp-ti_currency': event.ti_cu, 'x-sp-dvce_sent_tstamp': event.stm, 'x-sp-tp2': config.includeOriginalTp2Event ? event : undefined, }; const withCurrentEnv = withEnv(event, config); const result = [ withCurrentEnv(addSelfDescPropsTp2), withCurrentEnv(populateAdditionalProperties), withCurrentEnv(postprocess), cleanObject, ].reduce((acc, curr) => curr(acc), commonEvent); return result; }; /** * Returns the CDN URL of the requested JS tracker version. * * @param {string} spJsVersion - The requested JS tracker version * @returns {string} The URL */ const getSpJsLocation = (spJsVersion) => { const file = '/sp.js'; if (makeInteger(spJsVersion.charAt(0)) <= 2) return CDN.concat('/gh/snowplow/sp-js-assets@', spJsVersion, file); return CDN.concat('/npm/@snowplow/javascript-tracker@',spJsVersion,'/dist',file); }; /** * Filters the headers for the JS tracker request from jsDelivr. * * @param {Object} headers - The response headers from jsDelivr * @returns {Object} The final response headers */ const filterSpJsHeaders = (headers) => { const allowHeaders = ['content-type']; const finHeaders = {}; Object.keys(headers).forEach((header) => { if (allowHeaders.indexOf(header.toLowerCase()) > -1) finHeaders[header] = headers[header]; }); return finHeaders; }; /** * Handles the request for JS tracker library. * * @param {Array} requestPathParts - The request path's parts * @param {Object} cfg - The client configuration */ const handleSpJsRequest = (requestPathParts, cfg) => { const requestedSpJsVersion = requestPathParts[1]; const cacheVersionKey = 'snowplow_gtm_ss_client_version'; const contentKey = 'snowplow_js_' + requestedSpJsVersion; const headersKey = 'snowplow_js_headers_' + requestedSpJsVersion; // clear cache if necessary const cacheVersion = templateDataStorage.getItemCopy(cacheVersionKey); if (cacheVersion !== VERSION) { templateDataStorage.clear(); templateDataStorage.setItemCopy(cacheVersionKey, VERSION); } const cachedSpJs = templateDataStorage.getItemCopy(contentKey); const cachedSpJsHeaders = templateDataStorage.getItemCopy(headersKey) || {}; if (!cachedSpJs) { const spJsLocation = getSpJsLocation(requestedSpJsVersion); sendHttpGet( spJsLocation, (statusCode, headers, body) => { const responseHeaders = filterSpJsHeaders(headers); if (statusCode >= 200 && statusCode < 300) { templateDataStorage.setItemCopy(contentKey, body); templateDataStorage.setItemCopy(headersKey, responseHeaders); sendResponse(200, body, responseHeaders); } else { log('Failed to download sp.js: ', body); sendResponse(statusCode, body, responseHeaders); } }, { timeout: 5000 } ); } else { sendResponse(200, cachedSpJs, cachedSpJsHeaders); } }; /** * Determines if the incoming request is to get the JS tracker library * * @param {Array} requestPathParts - The request path's parts * @param {Object} cfg - The client configuration * @returns {boolean} Whether it is a request for sp.js */ const isSpJsRequest = (requestPathParts, cfg) => { if (requestPathParts.length < 3) { return false; } const spJsNames = [makeString(cfg.customSpJsName), 'sp.js']; const requestedSpJsName = requestPathParts[2]; if (spJsNames.indexOf(requestedSpJsName) === -1) { return false; } return true; }; // Main const requestParts = REQUEST_PATH.split('/'); // Check if request is for a Snowplow enriched event if (REQUEST_PATH === '/com.snowplowanalytics.snowplow/enriched') { // Claim the requst claimRequest(); log('Snowplow enriched request, claimed...'); let responseBody, events; const requestMethod = getRequestMethod(); if (requestMethod === 'POST') { events = enrichedPayloadToSnowplowEvents(getRequestBody()); responseBody = 'ok'; } else if (requestMethod === 'OPTIONS') { sendResponse(200); } if (events) { events.forEach((event) => { // Pass the event to a virtual container runContainer(mapSnowplowEnrichedEventToTagEvent(event, data), () => { log('Tags complete, sending response...'); sendResponse(200, responseBody); }); }); } } else if ( // Check if request is for the Snowplow tracker protocol v2 (tp2) or GET path, or a custom post path (data.claimGetRequests && REQUEST_PATH === '/i') || REQUEST_PATH === data.customPostPath || REQUEST_PATH === '/com.snowplowanalytics.snowplow/tp2' ) { // Claim the requst claimRequest(); log('Snowplow tracker protocol request, claimed...'); let responseBody, events; const requestMethod = getRequestMethod(); if (requestMethod === 'GET') { events = [getRequestQueryParameters()]; setPixelResponse(); } else if (requestMethod === 'POST') { events = payloadToSnowplowEvents(getRequestBody()); responseBody = 'ok'; } else if (requestMethod === 'OPTIONS') { sendResponse(200); } if (events) { events.forEach((event) => { // Pass the event to a virtual container runContainer(mapSnowplowTp2EventToTagEvent(event, data), () => { log('Tags complete, sending response...'); sendResponse(200, responseBody); }); }); } } else if (data.serveSpJs && isSpJsRequest(requestParts, data)) { claimRequest(); log('Snowplow sp.js request, claimed...'); handleSpJsRequest(requestParts, data); } ___SERVER_PERMISSIONS___ [ { "instance": { "key": { "publicId": "read_request", "versionId": "1" }, "param": [ { "key": "requestAccess", "value": { "type": 1, "string": "any" } }, { "key": "headerAccess", "value": { "type": 1, "string": "any" } }, { "key": "queryParameterAccess", "value": { "type": 1, "string": "any" } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true }, { "instance": { "key": { "publicId": "return_response", "versionId": "1" }, "param": [] }, "isRequired": true }, { "instance": { "key": { "publicId": "run_container", "versionId": "1" }, "param": [] }, "isRequired": true }, { "instance": { "key": { "publicId": "access_response", "versionId": "1" }, "param": [ { "key": "writeResponseAccess", "value": { "type": 1, "string": "any" } }, { "key": "writeHeaderAccess", "value": { "type": 1, "string": "specific" } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true }, { "instance": { "key": { "publicId": "logging", "versionId": "1" }, "param": [ { "key": "environments", "value": { "type": 1, "string": "debug" } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true }, { "instance": { "key": { "publicId": "access_template_storage", "versionId": "1" }, "param": [] }, "isRequired": true }, { "instance": { "key": { "publicId": "send_http", "versionId": "1" }, "param": [ { "key": "allowedUrls", "value": { "type": 1, "string": "specific" } }, { "key": "urls", "value": { "type": 2, "listItem": [ { "type": 1, "string": "https://cdn.jsdelivr.net/npm/@snowplow/javascript-tracker@*/dist/sp.js" }, { "type": 1, "string": "https://cdn.jsdelivr.net/gh/snowplow/sp-js-assets@*/sp.js" } ] } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true } ] ___TESTS___ scenarios: - name: v3 spjs proxied code: | const mockData = { ipInclude: true, populateGaProps: true, serveSpJs: true, customSpJsName: 'example.js', customPostPath: 'custom/path', claimGetRequests: true, includeOriginalTp2Event: true, includeOriginalSelfDescribingEvent: false, includeOriginalContextsArray: false, defaultUserId: true, defaultClientId: true, mergeEntities: false, mergeSelfDesc: false, }; // mocks mock('getRequestPath', '/3.0.0/sp.js'); const mockCDNHeaders = { 'Content-Type': 'application/javascript', 'Filter-Out': 'foo' }; const expectedHeaders = { 'Content-Type': 'application/javascript' }; let httpGetCallback; mock('sendHttpGet', (url, cb, opts) => { httpGetCallback = cb; cb(200, mockCDNHeaders, 'body'); }); const expectedSpJsKey = 'snowplow_js_3.0.0'; const expectedSpJsHeadersKey = 'snowplow_js_headers_3.0.0'; const prevSpJsStored = storage.getItemCopy(expectedSpJsKey); assertThat(prevSpJsStored).isNull(); runCode(mockData); assertApi('claimRequest').wasCalled(); assertApi('sendHttpGet').wasCalledWith( 'https://cdn.jsdelivr.net/npm/@snowplow/javascript-tracker@3.0.0/dist/sp.js', httpGetCallback, { timeout: 5000 } ); assertApi('setResponseHeader').wasCalledWith( 'Content-Type', 'application/javascript' ); assertApi('setResponseBody').wasCalledWith('body'); assertApi('getRequestHeader').wasCalledWith('origin'); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Origin', 'origin' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Credentials', 'true' ); assertApi('returnResponse').wasCalled(); const nextSpJsStored = storage.getItemCopy(expectedSpJsKey); assertThat(nextSpJsStored).isNotNull(); assertThat(nextSpJsStored).isEqualTo('body'); assertThat(storage.getItemCopy(expectedSpJsHeadersKey)).isEqualTo(expectedHeaders); assertThat(storage.getItemCopy('snowplow_gtm_ss_client_version')).isNotNull(); - name: v2 spjs proxied code: | const mockData = { ipInclude: true, populateGaProps: true, serveSpJs: true, customSpJsName: 'example.js', customPostPath: 'custom/path', claimGetRequests: true, includeOriginalTp2Event: true, includeOriginalSelfDescribingEvent: false, includeOriginalContextsArray: false, defaultUserId: true, defaultClientId: true, mergeEntities: false, mergeSelfDesc: false, }; // mocks mock('getRequestPath', () => '/2.18.0/sp.js'); let httpGetCallback; mock('sendHttpGet', (url, cb, opts) => { httpGetCallback = cb; cb(200, { 'Content-Type': 'application/javascript', 'filter-out': 'foo' }, 'body'); }); runCode(mockData); assertApi('claimRequest').wasCalled(); assertApi('sendHttpGet').wasCalledWith( 'https://cdn.jsdelivr.net/gh/snowplow/sp-js-assets@2.18.0/sp.js', httpGetCallback, { timeout: 5000 } ); assertApi('setResponseHeader').wasCalledWith('Content-Type', 'application/javascript'); assertApi('setResponseBody').wasCalledWith('body'); assertApi('getRequestHeader').wasCalledWith('origin'); assertApi('setResponseHeader').wasCalledWith('Access-Control-Allow-Origin', 'origin'); assertApi('setResponseHeader').wasCalledWith('Access-Control-Allow-Credentials', 'true'); assertApi('returnResponse').wasCalled(); - name: v3 spjs with custom name proxied code: | const mockData = { ipInclude: true, populateGaProps: true, serveSpJs: true, customSpJsName: 'example.js', customPostPath: 'custom/path', claimGetRequests: true, includeOriginalTp2Event: true, includeOriginalSelfDescribingEvent: false, includeOriginalContextsArray: false, defaultUserId: true, defaultClientId: true, mergeEntities: false, mergeSelfDesc: false, }; // mocks mock('getRequestPath', () => { return '/3.1.0/example.js'; }); let httpGetCallback; mock('sendHttpGet', (url, cb, opts) => { httpGetCallback = cb; cb(200, { 'Content-Type': 'application/javascript' }, 'body'); }); runCode(mockData); assertApi('claimRequest').wasCalled(); assertApi('sendHttpGet').wasCalledWith( 'https://cdn.jsdelivr.net/npm/@snowplow/javascript-tracker@3.1.0/dist/sp.js', httpGetCallback, { timeout: 5000 } ); assertApi('setResponseHeader').wasCalledWith( 'Content-Type', 'application/javascript' ); assertApi('setResponseBody').wasCalledWith('body'); assertApi('getRequestHeader').wasCalledWith('origin'); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Origin', 'origin' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Credentials', 'true' ); assertApi('returnResponse').wasCalled(); - name: v2 spjs with custom name proxied code: | const mockData = { ipInclude: true, populateGaProps: true, serveSpJs: true, customSpJsName: 'example.js', customPostPath: 'custom/path', claimGetRequests: true, includeOriginalTp2Event: true, includeOriginalSelfDescribingEvent: false, includeOriginalContextsArray: false, defaultUserId: true, defaultClientId: true, mergeEntities: false, mergeSelfDesc: false, }; // mocks mock('getRequestPath', () => { return '/2.18.1/example.js'; }); let httpGetCallback; mock('sendHttpGet', (url, cb, opts) => { httpGetCallback = cb; cb(200, { 'Content-Type': 'application/javascript' }, 'body'); }); runCode(mockData); assertApi('claimRequest').wasCalled(); assertApi('sendHttpGet').wasCalledWith( 'https://cdn.jsdelivr.net/gh/snowplow/sp-js-assets@2.18.1/sp.js', httpGetCallback, { timeout: 5000 } ); assertApi('setResponseHeader').wasCalledWith( 'Content-Type', 'application/javascript' ); assertApi('setResponseBody').wasCalledWith('body'); assertApi('getRequestHeader').wasCalledWith('origin'); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Origin', 'origin' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Credentials', 'true' ); assertApi('returnResponse').wasCalled(); - name: Container run with tp2 page view code: | const mockData = { ipInclude: true, populateGaProps: true, serveSpJs: true, customSpJsName: 'example.js', customPostPath: 'custom/path', claimGetRequests: true, includeOriginalTp2Event: true, includeOriginalSelfDescribingEvent: false, includeOriginalContextsArray: false, defaultUserId: true, defaultClientId: true, mergeEntities: false, mergeSelfDesc: false, }; const testEvent = page_view_tp2; // mocks mock('getRequestPath', '/com.snowplowanalytics.snowplow/tp2'); mock('getRequestMethod', 'POST'); mock('getRequestBody', json.stringify(testEvent)); let runContainerCallback; let resultingCommonEvent; mock('runContainer', (e, cb) => { resultingCommonEvent = e; runContainerCallback = cb; cb(); }); runCode(mockData); assertApi('claimRequest').wasCalled(); assertApi('setResponseStatus').wasCalledWith(200); assertApi('setResponseBody').wasCalledWith('ok'); assertApi('getRequestHeader').wasCalledWith('user-agent'); assertApi('getRequestHeader').wasCalledWith('host'); assertApi('getRequestHeader').wasCalledWith('referer'); assertApi('getRequestHeader').wasCalledWith('SP-Anonymous'); assertApi('getRequestHeader').wasCalledWith('origin'); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Origin', 'origin' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Credentials', 'true' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Headers', 'Content-Type, SP-Anonymous' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Methods', 'POST, GET, OPTIONS' ); assertApi('returnResponse').wasCalled(); const expectedCommonEvent = { event_name: 'page_view', client_id: 'd54a1904-7798-401a-be0b-1a83bea73634', language: 'en-GB', page_encoding: 'UTF-8', page_hostname: 'snowplowanalytics.com', page_location: 'https://snowplowanalytics.com/', 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-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-domain_userid': 'd54a1904-7798-401a-be0b-1a83bea73634', '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-tp2': { e: 'pv', url: 'https://snowplowanalytics.com/', page: 'Collect, manage and operationalize behavioral data at scale | Snowplow', tv: 'js-2.18.1', tna: 'sp', aid: 'website', p: 'web', tz: 'Europe/London', lang: 'en-GB', cs: 'UTF-8', res: '1920x1080', cd: '24', cookie: '1', eid: '8676de79-0eba-4435-ad95-8a41a8a0129c', dtm: '1628586512246', cx: 'eyJzY2hlbWEiOiJpZ2x1OmNvbS5zbm93cGxvd2FuYWx5dGljcy5zbm93cGxvdy9jb250ZXh0cy9qc29uc2NoZW1hLzEtMC0wIiwiZGF0YSI6W3sic2NoZW1hIjoiaWdsdTpjb20uc25vd3Bsb3dhbmFseXRpY3Muc25vd3Bsb3cvd2ViX3BhZ2UvanNvbnNjaGVtYS8xLTAtMCIsImRhdGEiOnsiaWQiOiJhODZjNDJlNS1iODMxLTQ1YzgtYjcwNi1lMjE0YzI2YjRiM2QifX0seyJzY2hlbWEiOiJpZ2x1Om9yZy53My9QZXJmb3JtYW5jZVRpbWluZy9qc29uc2NoZW1hLzEtMC0wIiwiZGF0YSI6eyJuYXZpZ2F0aW9uU3RhcnQiOjE2Mjg1ODY1MDg2MTAsInVubG9hZEV2ZW50U3RhcnQiOjAsInVubG9hZEV2ZW50RW5kIjowLCJyZWRpcmVjdFN0YXJ0IjowLCJyZWRpcmVjdEVuZCI6MCwiZmV0Y2hTdGFydCI6MTYyODU4NjUwODYxMCwiZG9tYWluTG9va3VwU3RhcnQiOjE2Mjg1ODY1MDg2MzcsImRvbWFpbkxvb2t1cEVuZCI6MTYyODU4NjUwODY5MSwiY29ubmVjdFN0YXJ0IjoxNjI4NTg2NTA4NjkxLCJjb25uZWN0RW5kIjoxNjI4NTg2NTA4NzYzLCJzZWN1cmVDb25uZWN0aW9uU3RhcnQiOjE2Mjg1ODY1MDg3MjEsInJlcXVlc3RTdGFydCI6MTYyODU4NjUwODc2MywicmVzcG9uc2VTdGFydCI6MTYyODU4NjUwODc5NywicmVzcG9uc2VFbmQiOjE2Mjg1ODY1MDg4MjEsImRvbUxvYWRpbmciOjE2Mjg1ODY1MDkwNzYsImRvbUludGVyYWN0aXZlIjoxNjI4NTg2NTA5MzgxLCJkb21Db250ZW50TG9hZGVkRXZlbnRTdGFydCI6MTYyODU4NjUwOTQwOCwiZG9tQ29udGVudExvYWRlZEV2ZW50RW5kIjoxNjI4NTg2NTA5NDE3LCJkb21Db21wbGV0ZSI6MTYyODU4NjUxMDMzMiwibG9hZEV2ZW50U3RhcnQiOjE2Mjg1ODY1MTAzMzIsImxvYWRFdmVudEVuZCI6MTYyODU4NjUxMDMzNH19XX0', vp: '745x1302', ds: '730x12393', vid: '1', sid: 'e7580b71-227b-4868-9ea9-322a263ce885', duid: 'd54a1904-7798-401a-be0b-1a83bea73634', stm: '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, }, ], 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', }; assertThat(resultingCommonEvent).isEqualTo(expectedCommonEvent); assertApi('runContainer').wasCalledWith( expectedCommonEvent, runContainerCallback ); - name: Container run with tp2 page view - with identified user code: | const funA = (x, ev) => x[0]; const mockData = { ipInclude: true, populateGaProps: true, serveSpJs: true, customSpJsName: 'example.js', customPostPath: 'custom/path', claimGetRequests: true, includeOriginalTp2Event: true, includeOriginalSelfDescribingEvent: false, includeOriginalContextsArray: false, defaultUserId: true, defaultClientId: true, mergeEntities: true, entityMergeRules: [ { schema: 'iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0', versionPolicy: 'control', prefix: 'x-test-', mergeLevel: 'rootLevel', customPath: '', keepOriginal: 'keep', customTransformFun: funA, }, { schema: 'x-sp-contexts_org_w3_performance_timing_1', versionPolicy: 'control', prefix: 'x-test-perf_timing_', mergeLevel: 'customPath', customPath: 'foobar', keepOriginal: 'discard', customTransformFun: '', }, ], mergeSelfDesc: false, }; const testEvent = json.parse(json.stringify(page_view_tp2)); testEvent.data[0].uid = 'snow123'; // mocks mock('getRequestPath', '/com.snowplowanalytics.snowplow/tp2'); mock('getRequestMethod', 'POST'); mock('getRequestBody', json.stringify(testEvent)); let runContainerCallback; let resultingCommonEvent; mock('runContainer', (e, cb) => { resultingCommonEvent = e; runContainerCallback = cb; cb(); }); runCode(mockData); assertApi('claimRequest').wasCalled(); assertApi('setResponseStatus').wasCalledWith(200); assertApi('setResponseBody').wasCalledWith('ok'); assertApi('getRequestHeader').wasCalledWith('user-agent'); assertApi('getRequestHeader').wasCalledWith('host'); assertApi('getRequestHeader').wasCalledWith('referer'); assertApi('getRequestHeader').wasCalledWith('SP-Anonymous'); assertApi('getRequestHeader').wasCalledWith('origin'); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Origin', 'origin' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Credentials', 'true' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Headers', 'Content-Type, SP-Anonymous' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Methods', 'POST, GET, OPTIONS' ); assertApi('returnResponse').wasCalled(); const expectedCommonEvent = { event_name: 'page_view', client_id: 'd54a1904-7798-401a-be0b-1a83bea73634', language: 'en-GB', page_encoding: 'UTF-8', page_hostname: 'snowplowanalytics.com', page_location: 'https://snowplowanalytics.com/', page_path: '/', page_referrer: 'referer', page_title: 'Collect, manage and operationalize behavioral data at scale | Snowplow', screen_resolution: '1920x1080', user_id: 'snow123', viewport_size: '745x1302', user_agent: 'user-agent', origin: 'origin', host: 'host', '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-domain_userid': 'd54a1904-7798-401a-be0b-1a83bea73634', 'x-sp-user_id': 'snow123', '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-tp2': { e: 'pv', url: 'https://snowplowanalytics.com/', page: 'Collect, manage and operationalize behavioral data at scale | Snowplow', tv: 'js-2.18.1', tna: 'sp', aid: 'website', p: 'web', tz: 'Europe/London', lang: 'en-GB', cs: 'UTF-8', res: '1920x1080', cd: '24', cookie: '1', eid: '8676de79-0eba-4435-ad95-8a41a8a0129c', dtm: '1628586512246', cx: 'eyJzY2hlbWEiOiJpZ2x1OmNvbS5zbm93cGxvd2FuYWx5dGljcy5zbm93cGxvdy9jb250ZXh0cy9qc29uc2NoZW1hLzEtMC0wIiwiZGF0YSI6W3sic2NoZW1hIjoiaWdsdTpjb20uc25vd3Bsb3dhbmFseXRpY3Muc25vd3Bsb3cvd2ViX3BhZ2UvanNvbnNjaGVtYS8xLTAtMCIsImRhdGEiOnsiaWQiOiJhODZjNDJlNS1iODMxLTQ1YzgtYjcwNi1lMjE0YzI2YjRiM2QifX0seyJzY2hlbWEiOiJpZ2x1Om9yZy53My9QZXJmb3JtYW5jZVRpbWluZy9qc29uc2NoZW1hLzEtMC0wIiwiZGF0YSI6eyJuYXZpZ2F0aW9uU3RhcnQiOjE2Mjg1ODY1MDg2MTAsInVubG9hZEV2ZW50U3RhcnQiOjAsInVubG9hZEV2ZW50RW5kIjowLCJyZWRpcmVjdFN0YXJ0IjowLCJyZWRpcmVjdEVuZCI6MCwiZmV0Y2hTdGFydCI6MTYyODU4NjUwODYxMCwiZG9tYWluTG9va3VwU3RhcnQiOjE2Mjg1ODY1MDg2MzcsImRvbWFpbkxvb2t1cEVuZCI6MTYyODU4NjUwODY5MSwiY29ubmVjdFN0YXJ0IjoxNjI4NTg2NTA4NjkxLCJjb25uZWN0RW5kIjoxNjI4NTg2NTA4NzYzLCJzZWN1cmVDb25uZWN0aW9uU3RhcnQiOjE2Mjg1ODY1MDg3MjEsInJlcXVlc3RTdGFydCI6MTYyODU4NjUwODc2MywicmVzcG9uc2VTdGFydCI6MTYyODU4NjUwODc5NywicmVzcG9uc2VFbmQiOjE2Mjg1ODY1MDg4MjEsImRvbUxvYWRpbmciOjE2Mjg1ODY1MDkwNzYsImRvbUludGVyYWN0aXZlIjoxNjI4NTg2NTA5MzgxLCJkb21Db250ZW50TG9hZGVkRXZlbnRTdGFydCI6MTYyODU4NjUwOTQwOCwiZG9tQ29udGVudExvYWRlZEV2ZW50RW5kIjoxNjI4NTg2NTA5NDE3LCJkb21Db21wbGV0ZSI6MTYyODU4NjUxMDMzMiwibG9hZEV2ZW50U3RhcnQiOjE2Mjg1ODY1MTAzMzIsImxvYWRFdmVudEVuZCI6MTYyODU4NjUxMDMzNH19XX0', vp: '745x1302', ds: '730x12393', vid: '1', sid: 'e7580b71-227b-4868-9ea9-322a263ce885', duid: 'd54a1904-7798-401a-be0b-1a83bea73634', stm: '1628586512248', uid: 'snow123', }, 'x-test-id': 'a86c42e5-b831-45c8-b706-e214c26b4b3d', 'x-sp-contexts_com_snowplowanalytics_snowplow_web_page_1': [ { id: 'a86c42e5-b831-45c8-b706-e214c26b4b3d' }, ], foobar: { 'x-test-perf_timing_navigationStart': 1628586508610, 'x-test-perf_timing_unloadEventStart': 0, 'x-test-perf_timing_unloadEventEnd': 0, 'x-test-perf_timing_redirectStart': 0, 'x-test-perf_timing_redirectEnd': 0, 'x-test-perf_timing_fetchStart': 1628586508610, 'x-test-perf_timing_domainLookupStart': 1628586508637, 'x-test-perf_timing_domainLookupEnd': 1628586508691, 'x-test-perf_timing_connectStart': 1628586508691, 'x-test-perf_timing_connectEnd': 1628586508763, 'x-test-perf_timing_secureConnectionStart': 1628586508721, 'x-test-perf_timing_requestStart': 1628586508763, 'x-test-perf_timing_responseStart': 1628586508797, 'x-test-perf_timing_responseEnd': 1628586508821, 'x-test-perf_timing_domLoading': 1628586509076, 'x-test-perf_timing_domInteractive': 1628586509381, 'x-test-perf_timing_domContentLoadedEventStart': 1628586509408, 'x-test-perf_timing_domContentLoadedEventEnd': 1628586509417, 'x-test-perf_timing_domComplete': 1628586510332, 'x-test-perf_timing_loadEventStart': 1628586510332, 'x-test-perf_timing_loadEventEnd': 1628586510334, }, 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', }; assertThat(resultingCommonEvent).isEqualTo(expectedCommonEvent); assertApi('runContainer').wasCalledWith( expectedCommonEvent, runContainerCallback ); - name: Container run with tp2 page-view and SP-Anonymous enabled code: | const mockData = { ipInclude: true, populateGaProps: true, serveSpJs: true, customSpJsName: 'example.js', customPostPath: 'custom/path', claimGetRequests: true, includeOriginalTp2Event: true, includeOriginalSelfDescribingEvent: false, includeOriginalContextsArray: false, defaultUserId: true, defaultClientId: true, mergeEntities: false, mergeSelfDesc: false, }; const testEvent = json.parse(json.stringify(page_view_tp2)); testEvent.data[0].uid = 'snow123'; // mocks mock('getRequestPath', '/com.snowplowanalytics.snowplow/tp2'); mock('getRequestMethod', 'POST'); mock('getRequestBody', json.stringify(testEvent)); mock('getRequestHeader', (header) => { if (header === 'SP-Anonymous') { return '*'; } return header; }); let runContainerCallback; let resultingCommonEvent; mock('runContainer', (e, cb) => { resultingCommonEvent = e; runContainerCallback = cb; cb(); }); runCode(mockData); assertApi('claimRequest').wasCalled(); assertApi('setResponseStatus').wasCalledWith(200); assertApi('setResponseBody').wasCalledWith('ok'); assertApi('getRequestHeader').wasCalledWith('user-agent'); assertApi('getRequestHeader').wasCalledWith('host'); assertApi('getRequestHeader').wasCalledWith('referer'); assertApi('getRequestHeader').wasCalledWith('SP-Anonymous'); assertApi('getRequestHeader').wasCalledWith('origin'); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Origin', 'origin' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Credentials', 'true' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Headers', 'Content-Type, SP-Anonymous' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Methods', 'POST, GET, OPTIONS' ); assertApi('returnResponse').wasCalled(); const expectedCommonEvent = { event_name: 'page_view', client_id: 'd54a1904-7798-401a-be0b-1a83bea73634', language: 'en-GB', page_encoding: 'UTF-8', page_hostname: 'snowplowanalytics.com', page_location: 'https://snowplowanalytics.com/', page_path: '/', page_referrer: 'referer', page_title: 'Collect, manage and operationalize behavioral data at scale | Snowplow', screen_resolution: '1920x1080', user_id: 'snow123', viewport_size: '745x1302', user_agent: 'user-agent', origin: 'origin', host: 'host', 'x-sp-anonymous': '*', '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-domain_userid': 'd54a1904-7798-401a-be0b-1a83bea73634', 'x-sp-user_id': 'snow123', '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-tp2': { e: 'pv', url: 'https://snowplowanalytics.com/', page: 'Collect, manage and operationalize behavioral data at scale | Snowplow', tv: 'js-2.18.1', tna: 'sp', aid: 'website', p: 'web', tz: 'Europe/London', lang: 'en-GB', cs: 'UTF-8', res: '1920x1080', cd: '24', cookie: '1', eid: '8676de79-0eba-4435-ad95-8a41a8a0129c', dtm: '1628586512246', cx: 'eyJzY2hlbWEiOiJpZ2x1OmNvbS5zbm93cGxvd2FuYWx5dGljcy5zbm93cGxvdy9jb250ZXh0cy9qc29uc2NoZW1hLzEtMC0wIiwiZGF0YSI6W3sic2NoZW1hIjoiaWdsdTpjb20uc25vd3Bsb3dhbmFseXRpY3Muc25vd3Bsb3cvd2ViX3BhZ2UvanNvbnNjaGVtYS8xLTAtMCIsImRhdGEiOnsiaWQiOiJhODZjNDJlNS1iODMxLTQ1YzgtYjcwNi1lMjE0YzI2YjRiM2QifX0seyJzY2hlbWEiOiJpZ2x1Om9yZy53My9QZXJmb3JtYW5jZVRpbWluZy9qc29uc2NoZW1hLzEtMC0wIiwiZGF0YSI6eyJuYXZpZ2F0aW9uU3RhcnQiOjE2Mjg1ODY1MDg2MTAsInVubG9hZEV2ZW50U3RhcnQiOjAsInVubG9hZEV2ZW50RW5kIjowLCJyZWRpcmVjdFN0YXJ0IjowLCJyZWRpcmVjdEVuZCI6MCwiZmV0Y2hTdGFydCI6MTYyODU4NjUwODYxMCwiZG9tYWluTG9va3VwU3RhcnQiOjE2Mjg1ODY1MDg2MzcsImRvbWFpbkxvb2t1cEVuZCI6MTYyODU4NjUwODY5MSwiY29ubmVjdFN0YXJ0IjoxNjI4NTg2NTA4NjkxLCJjb25uZWN0RW5kIjoxNjI4NTg2NTA4NzYzLCJzZWN1cmVDb25uZWN0aW9uU3RhcnQiOjE2Mjg1ODY1MDg3MjEsInJlcXVlc3RTdGFydCI6MTYyODU4NjUwODc2MywicmVzcG9uc2VTdGFydCI6MTYyODU4NjUwODc5NywicmVzcG9uc2VFbmQiOjE2Mjg1ODY1MDg4MjEsImRvbUxvYWRpbmciOjE2Mjg1ODY1MDkwNzYsImRvbUludGVyYWN0aXZlIjoxNjI4NTg2NTA5MzgxLCJkb21Db250ZW50TG9hZGVkRXZlbnRTdGFydCI6MTYyODU4NjUwOTQwOCwiZG9tQ29udGVudExvYWRlZEV2ZW50RW5kIjoxNjI4NTg2NTA5NDE3LCJkb21Db21wbGV0ZSI6MTYyODU4NjUxMDMzMiwibG9hZEV2ZW50U3RhcnQiOjE2Mjg1ODY1MTAzMzIsImxvYWRFdmVudEVuZCI6MTYyODU4NjUxMDMzNH19XX0', vp: '745x1302', ds: '730x12393', vid: '1', sid: 'e7580b71-227b-4868-9ea9-322a263ce885', duid: 'd54a1904-7798-401a-be0b-1a83bea73634', stm: '1628586512248', uid: 'snow123', }, '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, }, ], 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', }; assertThat(resultingCommonEvent).isEqualTo(expectedCommonEvent); assertApi('runContainer').wasCalledWith( expectedCommonEvent, runContainerCallback ); - name: Container run with /i GET page view code: | const mockData = { ipInclude: true, populateGaProps: true, serveSpJs: true, customSpJsName: 'example.js', customPostPath: 'custom/path', claimGetRequests: true, includeOriginalTp2Event: true, includeOriginalSelfDescribingEvent: false, includeOriginalContextsArray: false, defaultUserId: true, defaultClientId: true, mergeEntities: false, mergeSelfDesc: false, }; const testEvent = page_view_tp2; // mocks mock('getRequestPath', '/i'); mock('getRequestQueryParameters', testEvent.data[0]); mock('getRequestMethod', 'GET'); let runContainerCallback; let resultingCommonEvent; mock('runContainer', (e, cb) => { resultingCommonEvent = e; runContainerCallback = cb; cb(); }); runCode(mockData); assertApi('claimRequest').wasCalled(); assertApi('setResponseStatus').wasCalledWith(200); assertApi('setResponseBody').wasNotCalled(); assertApi('setPixelResponse').wasCalled(); assertApi('getRequestHeader').wasCalledWith('user-agent'); assertApi('getRequestHeader').wasCalledWith('host'); assertApi('getRequestHeader').wasCalledWith('referer'); assertApi('getRequestHeader').wasCalledWith('SP-Anonymous'); assertApi('getRequestHeader').wasCalledWith('origin'); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Origin', 'origin' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Credentials', 'true' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Headers', 'Content-Type, SP-Anonymous' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Methods', 'POST, GET, OPTIONS' ); assertApi('returnResponse').wasCalled(); const expectedCommonEvent = { event_name: 'page_view', client_id: 'd54a1904-7798-401a-be0b-1a83bea73634', language: 'en-GB', page_encoding: 'UTF-8', page_hostname: 'snowplowanalytics.com', page_location: 'https://snowplowanalytics.com/', 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-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-domain_userid': 'd54a1904-7798-401a-be0b-1a83bea73634', '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-tp2': { e: 'pv', url: 'https://snowplowanalytics.com/', page: 'Collect, manage and operationalize behavioral data at scale | Snowplow', tv: 'js-2.18.1', tna: 'sp', aid: 'website', p: 'web', tz: 'Europe/London', lang: 'en-GB', cs: 'UTF-8', res: '1920x1080', cd: '24', cookie: '1', eid: '8676de79-0eba-4435-ad95-8a41a8a0129c', dtm: '1628586512246', cx: 'eyJzY2hlbWEiOiJpZ2x1OmNvbS5zbm93cGxvd2FuYWx5dGljcy5zbm93cGxvdy9jb250ZXh0cy9qc29uc2NoZW1hLzEtMC0wIiwiZGF0YSI6W3sic2NoZW1hIjoiaWdsdTpjb20uc25vd3Bsb3dhbmFseXRpY3Muc25vd3Bsb3cvd2ViX3BhZ2UvanNvbnNjaGVtYS8xLTAtMCIsImRhdGEiOnsiaWQiOiJhODZjNDJlNS1iODMxLTQ1YzgtYjcwNi1lMjE0YzI2YjRiM2QifX0seyJzY2hlbWEiOiJpZ2x1Om9yZy53My9QZXJmb3JtYW5jZVRpbWluZy9qc29uc2NoZW1hLzEtMC0wIiwiZGF0YSI6eyJuYXZpZ2F0aW9uU3RhcnQiOjE2Mjg1ODY1MDg2MTAsInVubG9hZEV2ZW50U3RhcnQiOjAsInVubG9hZEV2ZW50RW5kIjowLCJyZWRpcmVjdFN0YXJ0IjowLCJyZWRpcmVjdEVuZCI6MCwiZmV0Y2hTdGFydCI6MTYyODU4NjUwODYxMCwiZG9tYWluTG9va3VwU3RhcnQiOjE2Mjg1ODY1MDg2MzcsImRvbWFpbkxvb2t1cEVuZCI6MTYyODU4NjUwODY5MSwiY29ubmVjdFN0YXJ0IjoxNjI4NTg2NTA4NjkxLCJjb25uZWN0RW5kIjoxNjI4NTg2NTA4NzYzLCJzZWN1cmVDb25uZWN0aW9uU3RhcnQiOjE2Mjg1ODY1MDg3MjEsInJlcXVlc3RTdGFydCI6MTYyODU4NjUwODc2MywicmVzcG9uc2VTdGFydCI6MTYyODU4NjUwODc5NywicmVzcG9uc2VFbmQiOjE2Mjg1ODY1MDg4MjEsImRvbUxvYWRpbmciOjE2Mjg1ODY1MDkwNzYsImRvbUludGVyYWN0aXZlIjoxNjI4NTg2NTA5MzgxLCJkb21Db250ZW50TG9hZGVkRXZlbnRTdGFydCI6MTYyODU4NjUwOTQwOCwiZG9tQ29udGVudExvYWRlZEV2ZW50RW5kIjoxNjI4NTg2NTA5NDE3LCJkb21Db21wbGV0ZSI6MTYyODU4NjUxMDMzMiwibG9hZEV2ZW50U3RhcnQiOjE2Mjg1ODY1MTAzMzIsImxvYWRFdmVudEVuZCI6MTYyODU4NjUxMDMzNH19XX0', vp: '745x1302', ds: '730x12393', vid: '1', sid: 'e7580b71-227b-4868-9ea9-322a263ce885', duid: 'd54a1904-7798-401a-be0b-1a83bea73634', stm: '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, }, ], 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', }; assertThat(resultingCommonEvent).isEqualTo(expectedCommonEvent); assertApi('runContainer').wasCalledWith( expectedCommonEvent, runContainerCallback ); - name: Container run with user_data context code: | const mockData = { ipInclude: true, populateGaProps: true, serveSpJs: true, customSpJsName: 'example.js', customPostPath: 'custom/path', claimGetRequests: true, includeOriginalTp2Event: false, includeOriginalSelfDescribingEvent: false, includeOriginalContextsArray: false, defaultUserId: true, defaultClientId: true, mergeEntities: true, entityMergeRules: [ { schema: 'iglu:org.whatwg/media_element/jsonschema/1-0-0', versionPolicy: 'free', prefix: '', mergeLevel: 'rootLevel', customPath: '', keepOriginal: 'discard', customTransformFun: '', }, { schema: 'contexts_com_snowplowanalytics_snowplow_media_player', versionPolicy: 'free', prefix: 'x-test-mp_', mergeLevel: 'customPath', customPath: 'user_data', keepOriginal: 'discard', customTransformFun: '', }, { schema: 'iglu:com.snowplowanalytics.snowplow/mobile_context/jsonschema/1-0-2', versionPolicy: 'control', prefix: '', mergeLevel: 'rootLevel', customPath: '', keepOriginal: 'discard', customTransformFun: '', }, { schema: 'x-sp-contexts_com_snowplowanalytics_snowplow_client_session', versionPolicy: 'free', prefix: '', mergeLevel: 'customPath', customPath: 'user_data.client_session.0', keepOriginal: 'keep', customTransformFun: '', }, ], mergeSelfDesc: true, selfDescMergeRules: [ { schema: 'iglu:com.snowplowanalytics.snowplow/media_player_event/jsonschema/1-0-0', versionPolicy: 'control', prefix: 'media_event_', mergeLevel: 'rootLevel', customPath: '', keepOriginal: 'keep', customTransformFun: '' }, ], }; const testEvent = mediaEventTp2; // mocks mock('getRequestPath', '/com.snowplowanalytics.snowplow/tp2'); mock('getRequestMethod', 'POST'); mock('getRequestBody', json.stringify(testEvent)); mock('getRequestHeader', (header) => { if (header === 'SP-Anonymous') { return '*'; } return header; }); let runContainerCallback; let resultingCommonEvent; mock('runContainer', (e, cb) => { resultingCommonEvent = e; runContainerCallback = cb; cb(); }); runCode(mockData); assertApi('claimRequest').wasCalled(); assertApi('setResponseStatus').wasCalledWith(200); assertApi('setResponseBody').wasCalledWith('ok'); assertApi('getRequestHeader').wasCalledWith('user-agent'); assertApi('getRequestHeader').wasCalledWith('host'); assertApi('getRequestHeader').wasCalledWith('referer'); assertApi('getRequestHeader').wasCalledWith('SP-Anonymous'); assertApi('getRequestHeader').wasCalledWith('origin'); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Origin', 'origin' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Credentials', 'true' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Headers', 'Content-Type, SP-Anonymous' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Methods', 'POST, GET, OPTIONS' ); assertApi('returnResponse').wasCalled(); const expectedCommonEvent = { event_name: 'media_player_event', language: 'en-US', page_encoding: 'windows-1252', page_hostname: 'localhost', page_location: 'http://localhost:8000/', page_path: '/', page_referrer: 'referer', screen_resolution: '1920x1080', viewport_size: '744x971', user_agent: 'user-agent', origin: 'origin', host: 'host', 'x-sp-anonymous': '*', 'x-sp-app_id': 'media-test', 'x-sp-platform': 'web', 'x-sp-dvce_created_tstamp': '1697609091769', 'x-sp-event_id': '011c85b9-0ee1-4f01-a9ca-7bd11db0c811', 'x-sp-name_tracker': 'spTest', 'x-sp-v_tracker': 'js-3.16.0', 'x-sp-domain_sessionid': '6ce287c3-e58d-4501-9586-1062d9b2d80c', 'x-sp-domain_sessionidx': 1, 'x-sp-domain_userid': 'fd97960a-bcb9-4530-8446-e370e1952e5e', 'x-sp-user_id': 'media_tester', 'x-sp-br_cookies': '1', 'x-sp-br_colordepth': '24', 'x-sp-br_viewwidth': 744, 'x-sp-br_viewheight': 971, 'x-sp-dvce_screenwidth': 1920, 'x-sp-dvce_screenheight': 1080, 'x-sp-doc_charset': 'windows-1252', 'x-sp-doc_width': 729, 'x-sp-doc_height': 1211, 'x-sp-dvce_sent_tstamp': '1697609091773', 'x-sp-self_describing_event_com_snowplowanalytics_snowplow_media_player_event_1': { type: 'pause' }, // contexts_org_whatwg_media_element_1 props at root level htmlId: 'bunny-mp4', mediaType: 'VIDEO', autoPlay: false, buffered: [{ start: 0, end: 13.424 }], controls: true, currentSrc: 'https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4', defaultMuted: false, defaultPlaybackRate: 1, // error: null, // gets cleaned-up by cleanObject networkState: 'NETWORK_LOADING', preload: 'metadata', readyState: 'HAVE_ENOUGH_DATA', seekable: [{ start: 0, end: 596.48 }], seeking: false, src: 'https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4', textTracks: [], fileExtension: 'mp4', fullscreen: false, pictureInPicture: false, 'x-sp-contexts_org_whatwg_video_element_1': [ { poster: '', videoHeight: 360, videoWidth: 640 }, ], 'x-sp-contexts_com_snowplowanalytics_snowplow_web_page_1': [ { id: '586b753d-c961-4852-a164-f641c9a4404f' }, ], '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', }, }, ], osType: 'testOsType', osVersion: 'testOsVersion', deviceManufacturer: 'testDevMan', deviceModel: 'testDevModel', 'x-sp-contexts_com_snowplowanalytics_snowplow_client_session_1': [ { userId: 'fd97960a-bcb9-4530-8446-e370e1952e5e', sessionId: '6ce287c3-e58d-4501-9586-1062d9b2d80c', eventIndex: 4, sessionIndex: 1, previousSessionId: null, storageMechanism: 'COOKIE_1', firstEventId: 'f8525402-1483-4fc4-8fc7-3eea2559127e', firstEventTimestamp: '2023-10-18T06:04:39.898Z', }, ], 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', }, 'x-test-mp_currentTime': 5.801593, 'x-test-mp_duration': 596.48, 'x-test-mp_ended': false, 'x-test-mp_loop': false, 'x-test-mp_muted': false, 'x-test-mp_paused': true, 'x-test-mp_playbackRate': 1, 'x-test-mp_volume': 100, client_session: [ { userId: 'fd97960a-bcb9-4530-8446-e370e1952e5e', sessionId: '6ce287c3-e58d-4501-9586-1062d9b2d80c', eventIndex: 4, sessionIndex: 1, previousSessionId: null, storageMechanism: 'COOKIE_1', firstEventId: 'f8525402-1483-4fc4-8fc7-3eea2559127e', firstEventTimestamp: '2023-10-18T06:04:39.898Z', }, ], }, ga_session_id: '6ce287c3-e58d-4501-9586-1062d9b2d80c', ga_session_number: '1', 'x-ga-mp2-seg': '1', 'x-ga-protocol_version': '2', 'x-ga-page_id': '586b753d-c961-4852-a164-f641c9a4404f', client_id: 'fd97960a-bcb9-4530-8446-e370e1952e5e', user_id: 'media_tester', media_event_type: 'pause', }; assertThat(resultingCommonEvent).isEqualTo(expectedCommonEvent); assertApi('runContainer').wasCalledWith( expectedCommonEvent, runContainerCallback ); - name: Container run with client_session context from mobile tracker code: | const mockData = { ipInclude: true, populateGaProps: true, serveSpJs: true, customSpJsName: 'example.js', customPostPath: 'custom/path', claimGetRequests: true, includeOriginalTp2Event: true, includeOriginalSelfDescribingEvent: false, includeOriginalContextsArray: false, defaultUserId: true, defaultClientId: true, mergeEntities: false, mergeSelfDesc: false, }; const testEvent = page_view_mobile; // mocks mock('getRequestPath', '/com.snowplowanalytics.snowplow/tp2'); mock('getRequestMethod', 'POST'); mock('getRequestBody', json.stringify(testEvent)); let runContainerCallback; let resultingCommonEvent; mock('runContainer', (e, cb) => { resultingCommonEvent = e; runContainerCallback = cb; cb(); }); runCode(mockData); assertApi('claimRequest').wasCalled(); assertApi('setResponseStatus').wasCalledWith(200); assertApi('setResponseBody').wasCalledWith('ok'); assertApi('getRequestHeader').wasCalledWith('user-agent'); assertApi('getRequestHeader').wasCalledWith('host'); assertApi('getRequestHeader').wasCalledWith('referer'); assertApi('getRequestHeader').wasCalledWith('SP-Anonymous'); assertApi('getRequestHeader').wasCalledWith('origin'); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Origin', 'origin' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Credentials', 'true' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Headers', 'Content-Type, SP-Anonymous' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Methods', 'POST, GET, OPTIONS' ); assertApi('returnResponse').wasCalled(); const expectedCommonEvent = { event_name: 'page_view', client_id: 'a4942035-de41-4bbd-b466-9c1ef7f7fb65', language: 'en-GB', page_encoding: 'UTF-8', page_hostname: 'snowplowanalytics.com', page_location: 'https://snowplowanalytics.com/', page_path: '/', page_referrer: 'referer', page_title: 'Collect, manage and operationalize behavioral data at scale | Snowplow', screen_resolution: '1920x1080', user_id: 'snow123', viewport_size: '745x1302', user_agent: 'user-agent', origin: 'origin', host: 'host', 'x-sp-app_id': 'my-app', 'x-sp-platform': 'mob', 'x-sp-dvce_created_tstamp': '1628586512246', 'x-sp-event_id': '8676de79-0eba-4435-ad95-8a41a8a0129c', 'x-sp-name_tracker': 'sp', 'x-sp-v_tracker': 'ios-2.0.0', 'x-sp-domain_sessionid': 'e7580b71-227b-4868-9ea9-322a263ce885', 'x-sp-domain_sessionidx': 1, 'x-sp-domain_userid': 'd54a1904-7798-401a-be0b-1a83bea73634', 'x-sp-user_id': 'snow123', '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-tp2': { e: 'pv', url: 'https://snowplowanalytics.com/', page: 'Collect, manage and operationalize behavioral data at scale | Snowplow', tv: 'ios-2.0.0', tna: 'sp', aid: 'my-app', p: 'mob', tz: 'Europe/London', lang: 'en-GB', cs: 'UTF-8', res: '1920x1080', cd: '24', cookie: '1', eid: '8676de79-0eba-4435-ad95-8a41a8a0129c', dtm: '1628586512246', cx: 'ewogICJzY2hlbWEiOiAiaWdsdTpjb20uc25vd3Bsb3dhbmFseXRpY3Muc25vd3Bsb3cvY29udGV4dHMvanNvbnNjaGVtYS8xLTAtMCIsCiAgImRhdGEiOiBbCiAgICB7CiAgICAgICJzY2hlbWEiOiAiaWdsdTpjb20uc25vd3Bsb3dhbmFseXRpY3Muc25vd3Bsb3cvd2ViX3BhZ2UvanNvbnNjaGVtYS8xLTAtMCIsCiAgICAgICJkYXRhIjogeyAiaWQiOiAiYTg2YzQyZTUtYjgzMS00NWM4LWI3MDYtZTIxNGMyNmI0YjNkIiB9CiAgICB9LAogICAgewogICAgICAic2NoZW1hIjogImlnbHU6b3JnLnczL1BlcmZvcm1hbmNlVGltaW5nL2pzb25zY2hlbWEvMS0wLTAiLAogICAgICAiZGF0YSI6IHsKICAgICAgICAibmF2aWdhdGlvblN0YXJ0IjogMTYyODU4NjUwODYxMCwKICAgICAgICAidW5sb2FkRXZlbnRTdGFydCI6IDAsCiAgICAgICAgInVubG9hZEV2ZW50RW5kIjogMCwKICAgICAgICAicmVkaXJlY3RTdGFydCI6IDAsCiAgICAgICAgInJlZGlyZWN0RW5kIjogMCwKICAgICAgICAiZmV0Y2hTdGFydCI6IDE2Mjg1ODY1MDg2MTAsCiAgICAgICAgImRvbWFpbkxvb2t1cFN0YXJ0IjogMTYyODU4NjUwODYzNywKICAgICAgICAiZG9tYWluTG9va3VwRW5kIjogMTYyODU4NjUwODY5MSwKICAgICAgICAiY29ubmVjdFN0YXJ0IjogMTYyODU4NjUwODY5MSwKICAgICAgICAiY29ubmVjdEVuZCI6IDE2Mjg1ODY1MDg3NjMsCiAgICAgICAgInNlY3VyZUNvbm5lY3Rpb25TdGFydCI6IDE2Mjg1ODY1MDg3MjEsCiAgICAgICAgInJlcXVlc3RTdGFydCI6IDE2Mjg1ODY1MDg3NjMsCiAgICAgICAgInJlc3BvbnNlU3RhcnQiOiAxNjI4NTg2NTA4Nzk3LAogICAgICAgICJyZXNwb25zZUVuZCI6IDE2Mjg1ODY1MDg4MjEsCiAgICAgICAgImRvbUxvYWRpbmciOiAxNjI4NTg2NTA5MDc2LAogICAgICAgICJkb21JbnRlcmFjdGl2ZSI6IDE2Mjg1ODY1MDkzODEsCiAgICAgICAgImRvbUNvbnRlbnRMb2FkZWRFdmVudFN0YXJ0IjogMTYyODU4NjUwOTQwOCwKICAgICAgICAiZG9tQ29udGVudExvYWRlZEV2ZW50RW5kIjogMTYyODU4NjUwOTQxNywKICAgICAgICAiZG9tQ29tcGxldGUiOiAxNjI4NTg2NTEwMzMyLAogICAgICAgICJsb2FkRXZlbnRTdGFydCI6IDE2Mjg1ODY1MTAzMzIsCiAgICAgICAgImxvYWRFdmVudEVuZCI6IDE2Mjg1ODY1MTAzMzQKICAgICAgfQogICAgfSwKICAgIHsKICAgICAgInNjaGVtYSI6ICJpZ2x1OmNvbS5zbm93cGxvd2FuYWx5dGljcy5zbm93cGxvdy9jbGllbnRfc2Vzc2lvbi9qc29uc2NoZW1hLzEtMC0xIiwKICAgICAgImRhdGEiOiB7IAogICAgICAgICJ1c2VySWQiOiAiYTQ5NDIwMzUtZGU0MS00YmJkLWI0NjYtOWMxZWY3ZjdmYjY1IiwKICAgICAgICAic2Vzc2lvbklkIjogImM1OTMzZDU4LWI4YzItNDlkZC1iYWQ1LTYxNTRkNzFhN2I5ZCIsCiAgICAgICAgInNlc3Npb25JbmRleCI6ICI1IgogICAgICB9CiAgICB9CiAgXQp9Cg', vp: '745x1302', ds: '730x12393', vid: '1', sid: 'e7580b71-227b-4868-9ea9-322a263ce885', duid: 'd54a1904-7798-401a-be0b-1a83bea73634', stm: '1628586512248', uid: 'snow123', }, '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_snowplowanalytics_snowplow_client_session_1': [ { userId: 'a4942035-de41-4bbd-b466-9c1ef7f7fb65', sessionId: 'c5933d58-b8c2-49dd-bad5-6154d71a7b9d', sessionIndex: '5', }, ], ga_session_id: 'c5933d58-b8c2-49dd-bad5-6154d71a7b9d', ga_session_number: '5', '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', }; assertThat(resultingCommonEvent).isEqualTo(expectedCommonEvent); assertApi('runContainer').wasCalledWith( expectedCommonEvent, runContainerCallback ); - name: Container run with enriched event code: | const makeTabMap = require('makeTableMap'); const parseUrl = require('parseUrl'); const funA = (ietfData, event) => { const result = {}; const cookiesMap = makeTabMap(ietfData, 'name', 'value'); const gaCookie = cookiesMap._ga; if (gaCookie) { result.new_client_id = gaCookie.split('.')[0]; } const parsedUrl = parseUrl(event.page_location); const hostname = parsedUrl.hostname.replace('www.', ''); if (hostname === 'snowplowanalytics.com') { result.something_new = 'test_value'; } return result; }; const funB = (linkClickData, event) => { const result = {}; result.target_url = linkClickData.targetUrl; result.element_id = linkClickData.elementId; return result; }; const mockData = { ipInclude: true, populateGaProps: true, serveSpJs: true, customSpJsName: 'example.js', customPostPath: 'custom/path', claimGetRequests: true, includeOriginalTp2Event: true, includeOriginalSelfDescribingEvent: false, includeOriginalContextsArray: false, defaultUserId: true, defaultClientId: true, mergeEntities: true, entityMergeRules: [ { schema: 'contexts_org_schema_web_page_1', versionPolicy: 'control', prefix: '', mergeLevel: 'customPath', customPath: 'foo.bar', keepOriginal: 'keep', customTransformFun: '', }, { schema: 'contexts_org_w3_performance_timing_1', versionPolicy: 'free', prefix: 'x_test_perf_', mergeLevel: 'rootLevel', customPath: '', keepOriginal: 'keep', customTransformFun: '', }, { schema: 'contexts_org_ietf_http_cookie_1', versionPolicy: 'free', prefix: '', mergeLevel: 'rootLevel', customPath: '', keepOriginal: 'discard', customTransformFun: funA, }, ], mergeSelfDesc: true, selfDescMergeRules: [ { schema: 'self_describing_event_com_snowplowanalytics_snowplow_link_click_1', versionPolicy: 'control', prefix: '', mergeLevel: 'customPath', customPath: 'link.click', keepOriginal: 'keep', customTransformFun: funB, }, ], }; const testEvent = enrichedLinkClick; // mocks mock('getRequestPath', '/com.snowplowanalytics.snowplow/enriched'); mock('getRequestMethod', 'POST'); mock('getRequestBody', json.stringify(testEvent)); let runContainerCallback; let resultingCommonEvent; mock('runContainer', (e, cb) => { resultingCommonEvent = e; runContainerCallback = cb; cb(); }); runCode(mockData); assertApi('claimRequest').wasCalled(); assertApi('setResponseStatus').wasCalledWith(200); assertApi('setResponseBody').wasCalledWith('ok'); assertApi('getRequestHeader').wasCalledWith('user-agent'); assertApi('getRequestHeader').wasCalledWith('host'); assertApi('getRequestHeader').wasCalledWith('referer'); assertApi('getRequestHeader').wasCalledWith('SP-Anonymous'); assertApi('getRequestHeader').wasCalledWith('origin'); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Origin', 'origin' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Credentials', 'true' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Headers', 'Content-Type, SP-Anonymous' ); assertApi('setResponseHeader').wasCalledWith( 'Access-Control-Allow-Methods', 'POST, GET, OPTIONS' ); assertApi('returnResponse').wasCalled(); const expectedCommonEvent = { event_name: 'link_click', client_id: 'bc2e92ec6c204a14', page_hostname: 'www.snowplowanalytics.com', page_location: 'http://www.snowplowanalytics.com', page_path: '/', page_referrer: 'referer', page_title: 'On Analytics', user_id: 'jon.doe@email.com', origin: 'origin', host: 'host', 'x-sp-app_id': 'angry-birds', 'x-sp-platform': 'web', 'x-sp-etl_tstamp': '2017-01-26T00:01:25.292Z', 'x-sp-collector_tstamp': '2013-11-26T00:02:05Z', 'x-sp-dvce_created_tstamp': '2013-11-26T00:03:57.885Z', 'x-sp-event': 'page_view', 'x-sp-event_id': 'c6ef3124-b53a-4b13-a233-0088f79dcbcb', 'x-sp-txn_id': 41828, 'x-sp-name_tracker': 'cloudfront-1', 'x-sp-v_tracker': 'js-2.1.0', 'x-sp-v_collector': 'clj-tomcat-0.1.0', 'x-sp-v_etl': 'serde-0.5.2', 'x-sp-user_fingerprint': '2161814971', 'x-sp-domain_sessionidx': 3, 'x-sp-domain_userid': 'bc2e92ec6c204a14', 'x-sp-user_id': 'jon.doe@email.com', 'x-sp-network_userid': 'ecdff4d0-9175-40ac-a8bb-325c49733607', 'x-sp-geo_country': 'US', 'x-sp-geo_region': 'TX', 'x-sp-geo_city': 'New York', 'x-sp-geo_zipcode': '94109', 'x-sp-geo_latitude': 37.443604, 'x-sp-geo_longitude': -122.4124, 'x-sp-geo_location': '37.443604,-122.4124', 'x-sp-geo_region_name': 'Florida', 'x-sp-ip_isp': 'FDN Communications', 'x-sp-ip_organization': 'Bouygues Telecom', 'x-sp-ip_domain': 'nuvox.net', 'x-sp-ip_netspeed': 'Cable/DSL', 'x-sp-page_urlscheme': 'http', 'x-sp-page_urlhost': 'www.snowplowanalytics.com', 'x-sp-page_urlport': 80, 'x-sp-page_urlpath': '/product/index.html', 'x-sp-page_urlquery': 'id=GTM-DLRG', 'x-sp-page_urlfragment': '4-conclusion', 'x-sp-br_features_pdf': true, 'x-sp-br_features_flash': false, 'x-sp-domain_sessionid': '2b15e5c8-d3b1-11e4-b9d6-1681e6b88ec1', 'x-sp-derived_tstamp': '2013-11-26T00:03:57.886Z', 'x-sp-event_vendor': 'com.snowplowanalytics.snowplow', 'x-sp-event_name': 'link_click', 'x-sp-event_format': 'jsonschema', 'x-sp-event_version': '1-0-0', 'x-sp-event_fingerprint': 'e3dbfa9cca0412c3d4052863cefb547f', 'x-sp-true_tstamp': '2013-11-26T00:03:57.886Z', 'x-sp-contexts_org_schema_web_page_1': [ { genre: 'blog', inLanguage: 'en-US', datePublished: '2014-11-06T00:00:00Z', author: 'Fred Blundun', breadcrumb: ['blog', 'releases'], keywords: ['snowplow', 'javascript', 'tracker', 'event'], }, ], foo: { bar: { genre: 'blog', inLanguage: 'en-US', datePublished: '2014-11-06T00:00:00Z', author: 'Fred Blundun', breadcrumb: ['blog', 'releases'], keywords: ['snowplow', 'javascript', 'tracker', 'event'], }, }, 'x-sp-contexts_org_w3_performance_timing_1': [ { navigationStart: 1415358089861, unloadEventStart: 1415358090270, unloadEventEnd: 1415358090287, redirectStart: 0, redirectEnd: 0, fetchStart: 1415358089870, domainLookupStart: 1415358090102, domainLookupEnd: 1415358090102, connectStart: 1415358090103, connectEnd: 1415358090183, requestStart: 1415358090183, responseStart: 1415358090265, responseEnd: 1415358090265, domLoading: 1415358090270, domInteractive: 1415358090886, domContentLoadedEventStart: 1415358090968, domContentLoadedEventEnd: 1415358091309, domComplete: 0, loadEventStart: 0, loadEventEnd: 0, }, ], x_test_perf_navigationStart: 1415358089861, x_test_perf_unloadEventStart: 1415358090270, x_test_perf_unloadEventEnd: 1415358090287, x_test_perf_redirectStart: 0, x_test_perf_redirectEnd: 0, x_test_perf_fetchStart: 1415358089870, x_test_perf_domainLookupStart: 1415358090102, x_test_perf_domainLookupEnd: 1415358090102, x_test_perf_connectStart: 1415358090103, x_test_perf_connectEnd: 1415358090183, x_test_perf_requestStart: 1415358090183, x_test_perf_responseStart: 1415358090265, x_test_perf_responseEnd: 1415358090265, x_test_perf_domLoading: 1415358090270, x_test_perf_domInteractive: 1415358090886, x_test_perf_domContentLoadedEventStart: 1415358090968, x_test_perf_domContentLoadedEventEnd: 1415358091309, x_test_perf_domComplete: 0, x_test_perf_loadEventStart: 0, x_test_perf_loadEventEnd: 0, 'x-sp-self_describing_event_com_snowplowanalytics_snowplow_link_click_1': { targetUrl: 'http://www.example.com', elementClasses: ['foreground'], elementId: 'exampleLink', }, 'x-sp-contexts_com_snowplowanalytics_snowplow_ua_parser_context_1': [ { useragentFamily: 'IE', useragentMajor: '7', useragentMinor: '0', useragentPatch: null, useragentVersion: 'IE 7.0', osFamily: 'Windows XP', osMajor: null, osMinor: null, osPatch: null, osPatchMinor: null, osVersion: 'Windows XP', deviceFamily: 'Other', }, ], ga_session_id: '2b15e5c8-d3b1-11e4-b9d6-1681e6b88ec1', ga_session_number: '3', 'x-ga-mp2-seg': '1', 'x-ga-protocol_version': '2', ip_override: '92.231.54.234', new_client_id: 'GA1', something_new: 'test_value', link: { click: { target_url: 'http://www.example.com', element_id: 'exampleLink', }, }, }; assertThat(resultingCommonEvent).isEqualTo(expectedCommonEvent); assertApi('runContainer').wasCalledWith( expectedCommonEvent, runContainerCallback ); - name: Container run with advanced common event settings code: | const mockData = { ipInclude: true, populateGaProps: true, serveSpJs: true, customSpJsName: 'sp.js', customPostPath: '/com.snowplowanalytics.snowplow/tp2', claimGetRequests: true, includeOriginalTp2Event: true, includeOriginalSelfDescribingEvent: true, includeOriginalContextsArray: true, defaultClientId: false, clientId: [ { priority: '10', propPath: 'x-sp-domain_userid', }, { priority: '20', propPath: 'x-sp-contexts_com_snowplowanalytics_snowplow_client_session_1.0.firstEventId', }, ], defaultUserId: false, userId: [ { priority: '10', propPath: 'x-sp-contexts_com_google_tag-manager_server-side_user_data_1.0.email_address', }, ], mergeEntities: false, mergeSelfDesc: false, }; const testEvent = mediaEventTp2; // mocks mock('getRequestPath', '/com.snowplowanalytics.snowplow/tp2'); mock('getRequestMethod', 'POST'); mock('getRequestBody', json.stringify(testEvent)); let runContainerCallback; let resultingCommonEvent; mock('runContainer', (e, cb) => { resultingCommonEvent = e; runContainerCallback = cb; cb(); }); runCode(mockData); assertApi('claimRequest').wasCalled(); assertApi('returnResponse').wasCalled(); const selfDescEvent = mediaEventTp2.data[0]; const expectedCommonEvent = { event_name: 'media_player_event', language: 'en-US', page_encoding: 'windows-1252', page_hostname: 'localhost', page_location: 'http://localhost:8000/', page_path: '/', page_referrer: 'referer', screen_resolution: '1920x1080', viewport_size: '744x971', user_agent: 'user-agent', origin: 'origin', host: 'host', ip_override: '1.2.3.4', 'x-sp-app_id': 'media-test', 'x-sp-platform': 'web', 'x-sp-dvce_created_tstamp': '1697609091769', 'x-sp-event_id': '011c85b9-0ee1-4f01-a9ca-7bd11db0c811', 'x-sp-name_tracker': 'spTest', 'x-sp-v_tracker': 'js-3.16.0', 'x-sp-domain_sessionid': '6ce287c3-e58d-4501-9586-1062d9b2d80c', 'x-sp-domain_sessionidx': 1, 'x-sp-domain_userid': 'fd97960a-bcb9-4530-8446-e370e1952e5e', 'x-sp-user_id': 'media_tester', 'x-sp-br_cookies': '1', 'x-sp-br_colordepth': '24', 'x-sp-br_viewwidth': 744, 'x-sp-br_viewheight': 971, 'x-sp-dvce_screenwidth': 1920, 'x-sp-dvce_screenheight': 1080, 'x-sp-doc_charset': 'windows-1252', 'x-sp-doc_width': 729, 'x-sp-doc_height': 1211, 'x-sp-dvce_sent_tstamp': '1697609091773', 'x-sp-tp2': selfDescEvent, 'x-sp-self_describing_event_com_snowplowanalytics_snowplow_media_player_event_1': { type: 'pause' }, 'x-sp-self_describing_event': json.parse(selfDescEvent.ue_pr).data, 'x-sp-contexts': json.parse(selfDescEvent.co).data, 'x-sp-contexts_org_whatwg_media_element_1': [ { htmlId: 'bunny-mp4', mediaType: 'VIDEO', autoPlay: false, buffered: [{ start: 0, end: 13.424 }], controls: true, currentSrc: 'https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4', defaultMuted: false, defaultPlaybackRate: 1, error: null, networkState: 'NETWORK_LOADING', preload: 'metadata', readyState: 'HAVE_ENOUGH_DATA', seekable: [{ start: 0, end: 596.48 }], seeking: false, src: 'https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4', textTracks: [], fileExtension: 'mp4', fullscreen: false, pictureInPicture: false, }, ], 'x-sp-contexts_com_snowplowanalytics_snowplow_media_player_1': [ { currentTime: 5.801593, duration: 596.48, ended: false, loop: false, muted: false, paused: true, playbackRate: 1, volume: 100, }, ], 'x-sp-contexts_org_whatwg_video_element_1': [ { poster: '', videoHeight: 360, videoWidth: 640 }, ], 'x-sp-contexts_com_snowplowanalytics_snowplow_web_page_1': [ { id: '586b753d-c961-4852-a164-f641c9a4404f' }, ], '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', }, }, ], 'x-sp-contexts_com_snowplowanalytics_snowplow_mobile_context_1': [ { osType: 'testOsType', osVersion: 'testOsVersion', deviceManufacturer: 'testDevMan', deviceModel: 'testDevModel', }, ], 'x-sp-contexts_com_snowplowanalytics_snowplow_client_session_1': [ { userId: 'fd97960a-bcb9-4530-8446-e370e1952e5e', sessionId: '6ce287c3-e58d-4501-9586-1062d9b2d80c', eventIndex: 4, sessionIndex: 1, previousSessionId: null, storageMechanism: 'COOKIE_1', firstEventId: 'f8525402-1483-4fc4-8fc7-3eea2559127e', firstEventTimestamp: '2023-10-18T06:04:39.898Z', }, ], 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: '6ce287c3-e58d-4501-9586-1062d9b2d80c', ga_session_number: '1', 'x-ga-mp2-seg': '1', 'x-ga-protocol_version': '2', 'x-ga-page_id': '586b753d-c961-4852-a164-f641c9a4404f', client_id: 'f8525402-1483-4fc4-8fc7-3eea2559127e', user_id: 'foo@example.com', }; assertThat(resultingCommonEvent).isEqualTo(expectedCommonEvent); assertApi('runContainer').wasCalledWith( expectedCommonEvent, runContainerCallback ); setup: |- const json = require('JSON'); const log = require('logToConsole'); const storage = require('templateDataStorage'); mock('getRequestHeader', (header) => { if (header === 'SP-Anonymous') { return undefined; } return header; }); mock('getRemoteAddress', '1.2.3.4'); mock('getCookieValues', (c) => { return [c]; }); mock('claimRequest', function () {}); const page_view_tp2 = { schema: 'iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-4', data: [ { e: 'pv', url: 'https://snowplowanalytics.com/', page: 'Collect, manage and operationalize behavioral data at scale | Snowplow', tv: 'js-2.18.1', tna: 'sp', aid: 'website', p: 'web', tz: 'Europe/London', lang: 'en-GB', cs: 'UTF-8', res: '1920x1080', cd: '24', cookie: '1', eid: '8676de79-0eba-4435-ad95-8a41a8a0129c', dtm: '1628586512246', cx: 'eyJzY2hlbWEiOiJpZ2x1OmNvbS5zbm93cGxvd2FuYWx5dGljcy5zbm93cGxvdy9jb250ZXh0cy9qc29uc2NoZW1hLzEtMC0wIiwiZGF0YSI6W3sic2NoZW1hIjoiaWdsdTpjb20uc25vd3Bsb3dhbmFseXRpY3Muc25vd3Bsb3cvd2ViX3BhZ2UvanNvbnNjaGVtYS8xLTAtMCIsImRhdGEiOnsiaWQiOiJhODZjNDJlNS1iODMxLTQ1YzgtYjcwNi1lMjE0YzI2YjRiM2QifX0seyJzY2hlbWEiOiJpZ2x1Om9yZy53My9QZXJmb3JtYW5jZVRpbWluZy9qc29uc2NoZW1hLzEtMC0wIiwiZGF0YSI6eyJuYXZpZ2F0aW9uU3RhcnQiOjE2Mjg1ODY1MDg2MTAsInVubG9hZEV2ZW50U3RhcnQiOjAsInVubG9hZEV2ZW50RW5kIjowLCJyZWRpcmVjdFN0YXJ0IjowLCJyZWRpcmVjdEVuZCI6MCwiZmV0Y2hTdGFydCI6MTYyODU4NjUwODYxMCwiZG9tYWluTG9va3VwU3RhcnQiOjE2Mjg1ODY1MDg2MzcsImRvbWFpbkxvb2t1cEVuZCI6MTYyODU4NjUwODY5MSwiY29ubmVjdFN0YXJ0IjoxNjI4NTg2NTA4NjkxLCJjb25uZWN0RW5kIjoxNjI4NTg2NTA4NzYzLCJzZWN1cmVDb25uZWN0aW9uU3RhcnQiOjE2Mjg1ODY1MDg3MjEsInJlcXVlc3RTdGFydCI6MTYyODU4NjUwODc2MywicmVzcG9uc2VTdGFydCI6MTYyODU4NjUwODc5NywicmVzcG9uc2VFbmQiOjE2Mjg1ODY1MDg4MjEsImRvbUxvYWRpbmciOjE2Mjg1ODY1MDkwNzYsImRvbUludGVyYWN0aXZlIjoxNjI4NTg2NTA5MzgxLCJkb21Db250ZW50TG9hZGVkRXZlbnRTdGFydCI6MTYyODU4NjUwOTQwOCwiZG9tQ29udGVudExvYWRlZEV2ZW50RW5kIjoxNjI4NTg2NTA5NDE3LCJkb21Db21wbGV0ZSI6MTYyODU4NjUxMDMzMiwibG9hZEV2ZW50U3RhcnQiOjE2Mjg1ODY1MTAzMzIsImxvYWRFdmVudEVuZCI6MTYyODU4NjUxMDMzNH19XX0', vp: '745x1302', ds: '730x12393', vid: '1', sid: 'e7580b71-227b-4868-9ea9-322a263ce885', duid: 'd54a1904-7798-401a-be0b-1a83bea73634', stm: '1628586512248', }, ], }; const mediaEventTp2 = { schema: 'iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-4', data: [ { e: 'ue', eid: '011c85b9-0ee1-4f01-a9ca-7bd11db0c811', tv: 'js-3.16.0', tna: 'spTest', aid: 'media-test', p: 'web', cookie: '1', cs: 'windows-1252', lang: 'en-US', res: '1920x1080', cd: '24', tz: 'Europe/Athens', dtm: '1697609091769', vp: '744x971', ds: '729x1211', vid: '1', sid: '6ce287c3-e58d-4501-9586-1062d9b2d80c', duid: 'fd97960a-bcb9-4530-8446-e370e1952e5e', uid: 'media_tester', url: 'http://localhost:8000/', ue_pr: '{"schema":"iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0","data":{"schema":"iglu:com.snowplowanalytics.snowplow/media_player_event/jsonschema/1-0-0","data":{"type":"pause"}}}', co: '{"schema":"iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0","data":[{"schema":"iglu:org.whatwg/media_element/jsonschema/1-0-0","data":{"htmlId":"bunny-mp4","mediaType":"VIDEO","autoPlay":false,"buffered":[{"start":0,"end":13.424}],"controls":true,"currentSrc":"https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4","defaultMuted":false,"defaultPlaybackRate":1,"error":null,"networkState":"NETWORK_LOADING","preload":"metadata","readyState":"HAVE_ENOUGH_DATA","seekable":[{"start":0,"end":596.48}],"seeking":false,"src":"https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4","textTracks":[],"fileExtension":"mp4","fullscreen":false,"pictureInPicture":false}},{"schema":"iglu:com.snowplowanalytics.snowplow/media_player/jsonschema/1-0-0","data":{"currentTime":5.801593,"duration":596.48,"ended":false,"loop":false,"muted":false,"paused":true,"playbackRate":1,"volume":100}},{"schema":"iglu:org.whatwg/video_element/jsonschema/1-0-0","data":{"poster":"","videoHeight":360,"videoWidth":640}},{"schema":"iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0","data":{"id":"586b753d-c961-4852-a164-f641c9a4404f"}},{"schema":"iglu:com.google.tag-manager.server-side/user_data/jsonschema/1-0-0","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"}}},{"schema":"iglu:com.snowplowanalytics.snowplow/mobile_context/jsonschema/1-0-2","data":{"osType":"testOsType","osVersion":"testOsVersion","deviceManufacturer":"testDevMan","deviceModel":"testDevModel"}},{"schema":"iglu:com.snowplowanalytics.snowplow/client_session/jsonschema/1-0-2","data":{"userId":"fd97960a-bcb9-4530-8446-e370e1952e5e","sessionId":"6ce287c3-e58d-4501-9586-1062d9b2d80c","eventIndex":4,"sessionIndex":1,"previousSessionId":null,"storageMechanism":"COOKIE_1","firstEventId":"f8525402-1483-4fc4-8fc7-3eea2559127e","firstEventTimestamp":"2023-10-18T06:04:39.898Z"}}]}', stm: '1697609091773', }, ], }; const page_view_mobile = { schema: 'iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-4', data: [ { e: 'pv', url: 'https://snowplowanalytics.com/', page: 'Collect, manage and operationalize behavioral data at scale | Snowplow', tv: 'ios-2.0.0', tna: 'sp', aid: 'my-app', p: 'mob', tz: 'Europe/London', lang: 'en-GB', cs: 'UTF-8', res: '1920x1080', cd: '24', cookie: '1', eid: '8676de79-0eba-4435-ad95-8a41a8a0129c', dtm: '1628586512246', cx: 'ewogICJzY2hlbWEiOiAiaWdsdTpjb20uc25vd3Bsb3dhbmFseXRpY3Muc25vd3Bsb3cvY29udGV4dHMvanNvbnNjaGVtYS8xLTAtMCIsCiAgImRhdGEiOiBbCiAgICB7CiAgICAgICJzY2hlbWEiOiAiaWdsdTpjb20uc25vd3Bsb3dhbmFseXRpY3Muc25vd3Bsb3cvd2ViX3BhZ2UvanNvbnNjaGVtYS8xLTAtMCIsCiAgICAgICJkYXRhIjogeyAiaWQiOiAiYTg2YzQyZTUtYjgzMS00NWM4LWI3MDYtZTIxNGMyNmI0YjNkIiB9CiAgICB9LAogICAgewogICAgICAic2NoZW1hIjogImlnbHU6b3JnLnczL1BlcmZvcm1hbmNlVGltaW5nL2pzb25zY2hlbWEvMS0wLTAiLAogICAgICAiZGF0YSI6IHsKICAgICAgICAibmF2aWdhdGlvblN0YXJ0IjogMTYyODU4NjUwODYxMCwKICAgICAgICAidW5sb2FkRXZlbnRTdGFydCI6IDAsCiAgICAgICAgInVubG9hZEV2ZW50RW5kIjogMCwKICAgICAgICAicmVkaXJlY3RTdGFydCI6IDAsCiAgICAgICAgInJlZGlyZWN0RW5kIjogMCwKICAgICAgICAiZmV0Y2hTdGFydCI6IDE2Mjg1ODY1MDg2MTAsCiAgICAgICAgImRvbWFpbkxvb2t1cFN0YXJ0IjogMTYyODU4NjUwODYzNywKICAgICAgICAiZG9tYWluTG9va3VwRW5kIjogMTYyODU4NjUwODY5MSwKICAgICAgICAiY29ubmVjdFN0YXJ0IjogMTYyODU4NjUwODY5MSwKICAgICAgICAiY29ubmVjdEVuZCI6IDE2Mjg1ODY1MDg3NjMsCiAgICAgICAgInNlY3VyZUNvbm5lY3Rpb25TdGFydCI6IDE2Mjg1ODY1MDg3MjEsCiAgICAgICAgInJlcXVlc3RTdGFydCI6IDE2Mjg1ODY1MDg3NjMsCiAgICAgICAgInJlc3BvbnNlU3RhcnQiOiAxNjI4NTg2NTA4Nzk3LAogICAgICAgICJyZXNwb25zZUVuZCI6IDE2Mjg1ODY1MDg4MjEsCiAgICAgICAgImRvbUxvYWRpbmciOiAxNjI4NTg2NTA5MDc2LAogICAgICAgICJkb21JbnRlcmFjdGl2ZSI6IDE2Mjg1ODY1MDkzODEsCiAgICAgICAgImRvbUNvbnRlbnRMb2FkZWRFdmVudFN0YXJ0IjogMTYyODU4NjUwOTQwOCwKICAgICAgICAiZG9tQ29udGVudExvYWRlZEV2ZW50RW5kIjogMTYyODU4NjUwOTQxNywKICAgICAgICAiZG9tQ29tcGxldGUiOiAxNjI4NTg2NTEwMzMyLAogICAgICAgICJsb2FkRXZlbnRTdGFydCI6IDE2Mjg1ODY1MTAzMzIsCiAgICAgICAgImxvYWRFdmVudEVuZCI6IDE2Mjg1ODY1MTAzMzQKICAgICAgfQogICAgfSwKICAgIHsKICAgICAgInNjaGVtYSI6ICJpZ2x1OmNvbS5zbm93cGxvd2FuYWx5dGljcy5zbm93cGxvdy9jbGllbnRfc2Vzc2lvbi9qc29uc2NoZW1hLzEtMC0xIiwKICAgICAgImRhdGEiOiB7IAogICAgICAgICJ1c2VySWQiOiAiYTQ5NDIwMzUtZGU0MS00YmJkLWI0NjYtOWMxZWY3ZjdmYjY1IiwKICAgICAgICAic2Vzc2lvbklkIjogImM1OTMzZDU4LWI4YzItNDlkZC1iYWQ1LTYxNTRkNzFhN2I5ZCIsCiAgICAgICAgInNlc3Npb25JbmRleCI6ICI1IgogICAgICB9CiAgICB9CiAgXQp9Cg', vp: '745x1302', ds: '730x12393', vid: '1', sid: 'e7580b71-227b-4868-9ea9-322a263ce885', duid: 'd54a1904-7798-401a-be0b-1a83bea73634', stm: '1628586512248', uid: 'snow123', }, ], }; const enrichedLinkClick = { geo_location: '37.443604,-122.4124', app_id: 'angry-birds', platform: 'web', etl_tstamp: '2017-01-26T00:01:25.292Z', collector_tstamp: '2013-11-26T00:02:05Z', dvce_created_tstamp: '2013-11-26T00:03:57.885Z', event: 'page_view', event_id: 'c6ef3124-b53a-4b13-a233-0088f79dcbcb', txn_id: 41828, name_tracker: 'cloudfront-1', v_tracker: 'js-2.1.0', v_collector: 'clj-tomcat-0.1.0', v_etl: 'serde-0.5.2', user_id: 'jon.doe@email.com', user_ipaddress: '92.231.54.234', user_fingerprint: '2161814971', domain_userid: 'bc2e92ec6c204a14', domain_sessionidx: 3, network_userid: 'ecdff4d0-9175-40ac-a8bb-325c49733607', geo_country: 'US', geo_region: 'TX', geo_city: 'New York', geo_zipcode: '94109', geo_latitude: 37.443604, geo_longitude: -122.4124, geo_region_name: 'Florida', ip_isp: 'FDN Communications', ip_organization: 'Bouygues Telecom', ip_domain: 'nuvox.net', ip_netspeed: 'Cable/DSL', page_url: 'http://www.snowplowanalytics.com', page_title: 'On Analytics', page_referrer: null, page_urlscheme: 'http', page_urlhost: 'www.snowplowanalytics.com', page_urlport: 80, page_urlpath: '/product/index.html', page_urlquery: 'id=GTM-DLRG', page_urlfragment: '4-conclusion', refr_urlscheme: null, refr_urlhost: null, refr_urlport: null, refr_urlpath: null, refr_urlquery: null, refr_urlfragment: null, refr_medium: null, refr_source: null, refr_term: null, mkt_medium: null, mkt_source: null, mkt_term: null, mkt_content: null, mkt_campaign: null, contexts_org_ietf_http_cookie_1: [ { name: '_ga', value: 'GA1.2.3' }, { name: '_ga_FOO', value: 'GS1.2.3' }, ], contexts_org_schema_web_page_1: [ { genre: 'blog', inLanguage: 'en-US', datePublished: '2014-11-06T00:00:00Z', author: 'Fred Blundun', breadcrumb: ['blog', 'releases'], keywords: ['snowplow', 'javascript', 'tracker', 'event'], }, ], contexts_org_w3_performance_timing_1: [ { navigationStart: 1415358089861, unloadEventStart: 1415358090270, unloadEventEnd: 1415358090287, redirectStart: 0, redirectEnd: 0, fetchStart: 1415358089870, domainLookupStart: 1415358090102, domainLookupEnd: 1415358090102, connectStart: 1415358090103, connectEnd: 1415358090183, requestStart: 1415358090183, responseStart: 1415358090265, responseEnd: 1415358090265, domLoading: 1415358090270, domInteractive: 1415358090886, domContentLoadedEventStart: 1415358090968, domContentLoadedEventEnd: 1415358091309, domComplete: 0, loadEventStart: 0, loadEventEnd: 0, }, ], se_category: null, se_action: null, se_label: null, se_property: null, se_value: null, unstruct_event_com_snowplowanalytics_snowplow_link_click_1: { targetUrl: 'http://www.example.com', elementClasses: ['foreground'], elementId: 'exampleLink', }, tr_orderid: null, tr_affiliation: null, tr_total: null, tr_tax: null, tr_shipping: null, tr_city: null, tr_state: null, tr_country: null, ti_orderid: null, ti_sku: null, ti_name: null, ti_category: null, ti_price: null, ti_quantity: null, pp_xoffset_min: null, pp_xoffset_max: null, pp_yoffset_min: null, pp_yoffset_max: null, useragent: null, br_name: null, br_family: null, br_version: null, br_type: null, br_renderengine: null, br_lang: null, br_features_pdf: true, br_features_flash: false, br_features_java: null, br_features_director: null, br_features_quicktime: null, br_features_realplayer: null, br_features_windowsmedia: null, br_features_gears: null, br_features_silverlight: null, br_cookies: null, br_colordepth: null, br_viewwidth: null, br_viewheight: null, os_name: null, os_family: null, os_manufacturer: null, os_timezone: null, dvce_type: null, dvce_ismobile: null, dvce_screenwidth: null, dvce_screenheight: null, doc_charset: null, doc_width: null, doc_height: null, tr_currency: null, tr_total_base: null, tr_tax_base: null, tr_shipping_base: null, ti_currency: null, ti_price_base: null, base_currency: null, geo_timezone: null, mkt_clickid: null, mkt_network: null, etl_tags: null, dvce_sent_tstamp: null, refr_domain_userid: null, refr_dvce_tstamp: null, contexts_com_snowplowanalytics_snowplow_ua_parser_context_1: [ { useragentFamily: 'IE', useragentMajor: '7', useragentMinor: '0', useragentPatch: null, useragentVersion: 'IE 7.0', osFamily: 'Windows XP', osMajor: null, osMinor: null, osPatch: null, osPatchMinor: null, osVersion: 'Windows XP', deviceFamily: 'Other', }, ], domain_sessionid: '2b15e5c8-d3b1-11e4-b9d6-1681e6b88ec1', derived_tstamp: '2013-11-26T00:03:57.886Z', event_vendor: 'com.snowplowanalytics.snowplow', event_name: 'link_click', event_format: 'jsonschema', event_version: '1-0-0', event_fingerprint: 'e3dbfa9cca0412c3d4052863cefb547f', true_tstamp: '2013-11-26T00:03:57.886Z', }; ___NOTES___ Created on 24/07/2020, 15:17:13