___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", "version": 1, "securityGroups": [], "displayName": "Data Client", "brand": { "id": "brand_dummy", "displayName": "Stape", "thumbnail": "data:image/png;base64,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" }, "description": "Use this Client as a mapper from Request Data to Event Data.", "containerContexts": [ "SERVER" ] } ___TEMPLATE_PARAMETERS___ [ { "type": "CHECKBOX", "name": "exposeFPIDCookie", "checkboxText": "Expose FPID Cookie", "simpleValueType": true, "help": "If enabled, the FPID cookie only accessible by the server and generated by the GA4 Client, will be duplicated to FPIDP cookie, which will be accessible from in the browser by JavaScript. \n\u003cbr/\u003e\nIt\u0027s highly recommend using this option only in case it is necessary." }, { "type": "CHECKBOX", "name": "httpOnlyCookie", "checkboxText": "Write the _dcid cookie as HttpOnly", "simpleValueType": true, "help": "If enabled, the _dcid cookie will be written with the HttpOnly flag, making it non-accessible in the browser by JavaScript.", "defaultValue": false }, { "type": "CHECKBOX", "name": "generateClientId", "checkboxText": "Always generate client_id parameter", "simpleValueType": true, "help": "If enabled, even if the `client_id` parameter is not determined from the request, it will still be generated. The `client_id` parameter is required by GA4 tags.", "defaultValue": true }, { "type": "CHECKBOX", "name": "prolongCookies", "checkboxText": "Automatically prolong Data Tag cookies", "simpleValueType": true, "help": "If enabled, cookies generated by Data Tag will be reset from the server with an expiration time of 2 years. Its\u0027 useful if you use Data Tag store functionality.", "defaultValue": true }, { "type": "CHECKBOX", "name": "acceptMultipleEvents", "checkboxText": "Accept Multiple Events", "simpleValueType": true, "help": "When the Accept Multiple Events is set to true, the Data Client will parse an array of objects, in the request body, as separate events.\n\u003cbr/\u003e\nExample:\n\u003cbr/\u003e\n[\n {\"event\":\"page_view\"},\n {\"event\":\"view_item\"}\n]", "defaultValue": false }, { "type": "GROUP", "name": "responseSettings", "displayName": "Response Settings", "groupStyle": "ZIPPY_CLOSED", "subParams": [ { "type": "SELECT", "name": "responseStatusCode", "displayName": "Response Status Code", "selectItems": [ { "value": 200, "displayValue": "200" }, { "value": 201, "displayValue": "201" }, { "value": 301, "displayValue": "301" }, { "value": 302, "displayValue": "302" }, { "value": 403, "displayValue": "403" }, { "value": 404, "displayValue": "404" } ], "simpleValueType": true, "defaultValue": 200 }, { "type": "GROUP", "name": "regularResponseSettings", "groupStyle": "NO_ZIPPY", "subParams": [ { "type": "SELECT", "name": "responseBody", "displayName": "Response Body", "macrosInSelect": false, "selectItems": [ { "value": "timestamp", "displayValue": "JSON Object with timestamp (recommended)" }, { "value": "eventData", "displayValue": "JSON Object with Event Data" }, { "value": "empty", "displayValue": "Empty" } ], "simpleValueType": true, "defaultValue": "timestamp" }, { "type": "CHECKBOX", "name": "responseBodyGet", "checkboxText": "Send Response Body for GET request", "simpleValueType": true, "help": "By default, for the GET request type, the answer is image pixel. \u003ca target\u003d\"_blank\" href\u003d\"https://developers.google.com/tag-manager/serverside/api#setpixelresponse\"\u003eMore Info\u003c/a\u003e." } ], "enablingConditions": [ { "paramName": "responseStatusCode", "paramValue": 200, "type": "EQUALS" }, { "paramName": "responseStatusCode", "paramValue": 201, "type": "EQUALS" } ] }, { "type": "GROUP", "name": "redirectResponseSettings", "groupStyle": "NO_ZIPPY", "subParams": [ { "type": "TEXT", "name": "redirectTo", "displayName": "Redirect To", "simpleValueType": true, "valueValidators": [ { "type": "NON_EMPTY" }, { "type": "REGEX", "args": [ "^https?:\\/\\/.*" ] } ] }, { "type": "CHECKBOX", "name": "lookupForRedirectToParam", "checkboxText": "Try to find redirect destination in query params", "simpleValueType": true, "help": "Override destination URL with query param from request url" }, { "type": "TEXT", "name": "redirectToQueryParamName", "displayName": "Query Param Name", "simpleValueType": true, "enablingConditions": [ { "paramName": "lookupForRedirectToParam", "paramValue": true, "type": "EQUALS" } ], "valueValidators": [ { "type": "NON_EMPTY" } ] } ], "enablingConditions": [ { "paramName": "responseStatusCode", "paramValue": 301, "type": "EQUALS" }, { "paramName": "responseStatusCode", "paramValue": 302, "type": "EQUALS" } ] }, { "type": "GROUP", "name": "clientErrorResponseSettings", "groupStyle": "NO_ZIPPY", "subParams": [ { "type": "TEXT", "name": "clientErrorResponseMessage", "displayName": "Client Error Response Message", "simpleValueType": true, "valueValidators": [ { "type": "NON_EMPTY" } ] } ], "enablingConditions": [ { "paramName": "responseStatusCode", "paramValue": 403, "type": "EQUALS" }, { "paramName": "responseStatusCode", "paramValue": 404, "type": "EQUALS" } ] } ] }, { "type": "GROUP", "name": "pathSettings", "displayName": "Accepted Path Settings", "groupStyle": "ZIPPY_CLOSED", "subParams": [ { "type": "SIMPLE_TABLE", "name": "path", "displayName": "Type additional paths that will be claimed by this client", "simpleTableColumns": [ { "defaultValue": "", "displayName": "For example: /callback", "name": "path", "type": "TEXT", "isUnique": true, "valueValidators": [ { "type": "NON_EMPTY" } ] } ], "newRowButtonText": "Add path", "help": "By default path \u003cb\u003e/data\u003c/b\u003e will be claimed. But you can add more paths that will be claimed by this client." } ] } ] ___SANDBOXED_JS_FOR_SERVER___ const returnResponse = require('returnResponse'); const runContainer = require('runContainer'); const setResponseHeader = require('setResponseHeader'); const setResponseStatus = require('setResponseStatus'); const setResponseBody = require('setResponseBody'); const JSON = require('JSON'); const fromBase64 = require('fromBase64'); const getTimestampMillis = require('getTimestampMillis'); const getCookieValues = require('getCookieValues'); const getRequestBody = require('getRequestBody'); const getRequestMethod = require('getRequestMethod'); const getRequestHeader = require('getRequestHeader'); const getRequestPath = require('getRequestPath'); const getRequestQueryParameters = require('getRequestQueryParameters'); const makeInteger = require('makeInteger'); const getRemoteAddress = require('getRemoteAddress'); const setCookie = require('setCookie'); const setPixelResponse = require('setPixelResponse'); const generateRandom = require('generateRandom'); const computeEffectiveTldPlusOne = require('computeEffectiveTldPlusOne'); const getRequestQueryParameter = require('getRequestQueryParameter'); const getType = require('getType'); const decodeUriComponent = require('decodeUriComponent'); const createRegex = require('createRegex'); const makeString = require('makeString'); const Object = require('Object'); /*============================================================================== ==============================================================================*/ const requestMethod = getRequestMethod(); const path = getRequestPath(); let isClientUsed = false; let isEventModelsWrappedInArray = false; if (path === '/data') { runClient(); } if (data.path && !isClientUsed) { for (let key in data.path) { if (!isClientUsed && data.path[key].path === path) { runClient(); } } } /*============================================================================== Vendor related functions ==============================================================================*/ function runClient() { isClientUsed = true; require('claimRequest')(); if (requestMethod === 'OPTIONS') { setCommonResponseHeaders(200); returnResponse(); return; } const baseEventModel = getBaseEventModelWithQueryParameters(); let eventModels = getEventModels(baseEventModel); const clientId = getClientId(eventModels); eventModels = eventModels.map((eventModel) => { eventModel = addRequiredParametersToEventModel(eventModel); eventModel = addCommonParametersToEventModel(eventModel); eventModel = addClientIdToEventModel(eventModel, clientId); if (eventModel._dcid_temp) Object.delete(eventModel, '_dcid_temp'); return eventModel; }); storeClientId(eventModels[0]); exposeFPIDCookie(eventModels[0]); prolongDataTagCookies(eventModels[0]); const responseStatusCode = makeInteger(data.responseStatusCode || 200); setCommonResponseHeaders(responseStatusCode); let counter = 0; eventModels.forEach((event) => { runContainer(event, () => { if (++counter === eventModels.length) { switch (responseStatusCode) { case 200: case 201: if (requestMethod === 'POST' || data.responseBodyGet) { prepareResponseBody(eventModels); } else { setPixelResponse(); } break; case 301: case 302: setRedirectLocation(); break; case 403: case 404: setClientErrorResponseMessage(); break; } returnResponse(); } }); }); } function addCommonParametersToEventModel(eventModel) { if (!eventModel.ip_override) { if (eventModel.ip) eventModel.ip_override = eventModel.ip; else if (eventModel.ipOverride) eventModel.ip_override = eventModel.ipOverride; else eventModel.ip_override = getRemoteAddress(); } if (!eventModel.user_agent) { if (eventModel.userAgent) eventModel.user_agent = eventModel.userAgent; else if (getRequestHeader('User-Agent')) eventModel.user_agent = getRequestHeader('User-Agent'); } if (!eventModel.language) { const acceptLanguageHeader = getRequestHeader('Accept-Language'); if (acceptLanguageHeader) { eventModel.language = acceptLanguageHeader .split(';')[0] .substring(0, 2) .toLowerCase(); } } if (!eventModel.page_hostname) { if (eventModel.pageHostname) eventModel.page_hostname = eventModel.pageHostname; else if (eventModel.hostname) eventModel.page_hostname = eventModel.hostname; } if (!eventModel.page_location) { if (eventModel.pageLocation) eventModel.page_location = eventModel.pageLocation; else if (eventModel.url) eventModel.page_location = eventModel.url; else if (eventModel.href) eventModel.page_location = eventModel.href; } if (!eventModel.page_referrer) { if (eventModel.pageReferrer) eventModel.page_referrer = eventModel.pageReferrer; else if (eventModel.referrer) eventModel.page_referrer = eventModel.referrer; else if (eventModel.urlref) eventModel.page_referrer = eventModel.urlref; } if (!eventModel.value && eventModel.e_v) eventModel.value = eventModel.e_v; if (getType(eventModel.items) === 'array' && eventModel.items.length) { const firstItem = eventModel.items[0]; if (!eventModel.currency && firstItem.currency) eventModel.currency = firstItem.currency; if (eventModel.items.length === 1) { if (!eventModel.item_id && firstItem.item_id) eventModel.item_id = firstItem.item_id; if (!eventModel.item_name && firstItem.item_name) eventModel.item_name = firstItem.item_name; if (!eventModel.item_brand && firstItem.item_brand) eventModel.item_brand = firstItem.item_brand; if (!eventModel.item_quantity && firstItem.quantity) eventModel.item_quantity = firstItem.quantity; if (!eventModel.item_category && firstItem.item_category) eventModel.item_category = firstItem.item_category; if (!eventModel.item_price && firstItem.price) eventModel.item_price = firstItem.price; } if (!eventModel.value) { const valueFromItems = eventModel.items.reduce((acc, item) => { if (!item.price) return acc; const quantity = item.quantity ? item.quantity : 1; return acc + quantity * item.price; }, 0); if (valueFromItems) eventModel.value = valueFromItems; } } const ecommerceAction = getEcommerceAction(eventModel); if (ecommerceAction) { if (!eventModel['x-ga-mp1-pa']) eventModel['x-ga-mp1-pa'] = ecommerceAction; if ( ecommerceAction === 'purchase' && eventModel.ecommerce.purchase.actionField ) { if (!eventModel['x-ga-mp1-tr']) eventModel['x-ga-mp1-tr'] = eventModel.ecommerce.purchase.actionField.revenue; if (!eventModel.revenue) eventModel.revenue = eventModel.ecommerce.purchase.actionField.revenue; if (!eventModel.affiliation) eventModel.affiliation = eventModel.ecommerce.purchase.actionField.affiliation; if (!eventModel.tax) eventModel.tax = eventModel.ecommerce.purchase.actionField.tax; if (!eventModel.shipping) eventModel.shipping = eventModel.ecommerce.purchase.actionField.shipping; if (!eventModel.coupon) eventModel.coupon = eventModel.ecommerce.purchase.actionField.coupon; if (!eventModel.transaction_id) eventModel.transaction_id = eventModel.ecommerce.purchase.actionField.id; } } if (!eventModel.page_encoding && eventModel.pageEncoding) eventModel.page_encoding = eventModel.pageEncoding; if (!eventModel.page_path && eventModel.pagePath) eventModel.page_path = eventModel.pagePath; if (!eventModel.page_title && eventModel.pageTitle) eventModel.page_title = eventModel.pageTitle; if (!eventModel.screen_resolution && eventModel.screenResolution) eventModel.screen_resolution = eventModel.screenResolution; if (!eventModel.viewport_size && eventModel.viewportSize) eventModel.viewport_size = eventModel.viewportSize; if (!eventModel.user_id && eventModel.userId) eventModel.user_id = eventModel.userId; if (!eventModel.user_data) { let userData = {}; let userAddressData = {}; if (!userData.email_address) { if (eventModel.userEmail) userData.email_address = eventModel.userEmail; else if (eventModel.email_address) userData.email_address = eventModel.email_address; else if (eventModel.email) userData.email_address = eventModel.email; else if (eventModel.mail) userData.email_address = eventModel.mail; } if (!userData.phone_number) { if (eventModel.userPhoneNumber) userData.phone_number = eventModel.userPhoneNumber; else if (eventModel.phone_number) userData.phone_number = eventModel.phone_number; else if (eventModel.phoneNumber) userData.phone_number = eventModel.phoneNumber; else if (eventModel.phone) userData.phone_number = eventModel.phone; } if (!userAddressData.street && eventModel.street) userAddressData.street = eventModel.street; if (!userAddressData.city && eventModel.city) userAddressData.city = eventModel.city; if (!userAddressData.region && eventModel.region) userAddressData.region = eventModel.region; if (!userAddressData.country && eventModel.country) userAddressData.country = eventModel.country; if (!userAddressData.first_name) { if (eventModel.userFirstName) userAddressData.first_name = eventModel.userFirstName; else if (eventModel.first_name) userAddressData.first_name = eventModel.first_name; else if (eventModel.firstName) userAddressData.first_name = eventModel.firstName; else if (eventModel.name) userAddressData.first_name = eventModel.name; } if (!userAddressData.last_name) { if (eventModel.userLastName) userAddressData.last_name = eventModel.userLastName; else if (eventModel.last_name) userAddressData.last_name = eventModel.last_name; else if (eventModel.lastName) userAddressData.last_name = eventModel.lastName; else if (eventModel.surname) userAddressData.last_name = eventModel.surname; else if (eventModel.family_name) userAddressData.last_name = eventModel.family_name; else if (eventModel.familyName) userAddressData.last_name = eventModel.familyName; } if (!userAddressData.region) { if (eventModel.region) userAddressData.region = eventModel.region; else if (eventModel.state) userAddressData.region = eventModel.state; } if (!userAddressData.postal_code) { if (eventModel.postal_code) userAddressData.postal_code = eventModel.postal_code; else if (eventModel.postalCode) userAddressData.postal_code = eventModel.postalCode; else if (eventModel.zip) userAddressData.postal_code = eventModel.zip; } if (getObjectLength(userAddressData) !== 0) { userData.address = userAddressData; } if (!eventModel.user_data && getObjectLength(userData) !== 0) { eventModel.user_data = userData; } } return eventModel; } function getBaseEventModelWithQueryParameters() { const requestQueryParameters = getRequestQueryParameters(); const eventModel = {}; if (requestQueryParameters) { for (let queryParameterKey in requestQueryParameters) { if ( (queryParameterKey === 'dtcd' || queryParameterKey === 'dtdc') && requestMethod === 'GET' ) { let dt = queryParameterKey === 'dtcd' ? JSON.parse(requestQueryParameters[queryParameterKey]) : JSON.parse(fromBase64(requestQueryParameters[queryParameterKey])); for (let dtKey in dt) { eventModel[dtKey] = dt[dtKey]; } } else { eventModel[queryParameterKey] = requestQueryParameters[queryParameterKey]; } } } return eventModel; } function addClientIdToEventModel(eventModel, clientId) { eventModel.client_id = clientId; return eventModel; } function prolongDataTagCookies(eventModel) { if (data.prolongCookies) { let stapeData = getCookieValues('stape'); if (stapeData.length) { setCookie('stape', stapeData[0], { domain: 'auto', path: '/', samesite: getCookieType(eventModel), secure: true, 'max-age': 63072000, // 2 years httpOnly: false }); } } } function addRequiredParametersToEventModel(eventModel) { if (!eventModel.event_name) { let eventName = 'Data'; if (eventModel.eventName) eventName = eventModel.eventName; else if (eventModel.event) eventName = eventModel.event; else if (eventModel.e_n) eventName = eventModel.e_n; eventModel.event_name = eventName; } return eventModel; } function exposeFPIDCookie(eventModel) { if (data.exposeFPIDCookie) { let fpid = getCookieValues('FPID'); if (fpid.length) { setCookie('FPIDP', fpid[0], { domain: 'auto', path: '/', samesite: getCookieType(eventModel), secure: true, 'max-age': 63072000, // 2 years httpOnly: false }); } } } function storeClientId(eventModel) { if (data.generateClientId) { setCookie('_dcid', eventModel.client_id, { domain: 'auto', path: '/', samesite: getCookieType(eventModel), secure: true, 'max-age': 63072000, // 2 years httpOnly: data.httpOnlyCookie || false }); } } function setCommonResponseHeaders(statusCode) { setResponseHeader('Access-Control-Max-Age', '600'); setResponseHeader('Access-Control-Allow-Origin', getRequestHeader('origin')); setResponseHeader( 'Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS' ); setResponseHeader( 'Access-Control-Allow-Headers', 'content-type,set-cookie,x-robots-tag,x-gtm-server-preview,x-stape-preview,x-stape-app-version' ); setResponseHeader('Access-Control-Allow-Credentials', 'true'); setResponseStatus(statusCode); } function getCookieType(eventModel) { if (!eventModel.page_location) { return 'Lax'; } const host = getRequestHeader('host'); const effectiveTldPlusOne = computeEffectiveTldPlusOne( eventModel.page_location ); if (!host || !effectiveTldPlusOne) { return 'Lax'; } if (host && host.indexOf(effectiveTldPlusOne) !== -1) { return 'Lax'; } return 'None'; } function prepareResponseBody(eventModels) { if (data.responseBody === 'empty') { return; } const responseModel = isEventModelsWrappedInArray ? eventModels[0] : eventModels; setResponseHeader('Content-Type', 'application/json'); if (data.responseBody === 'eventData') { setResponseBody(JSON.stringify(responseModel)); return; } if (isEventModelsWrappedInArray) { setResponseBody( JSON.stringify({ timestamp: responseModel.timestamp, unique_event_id: responseModel.unique_event_id }) ); return; } setResponseBody( JSON.stringify( eventModels.map((eventModel) => { return { timestamp: eventModel.timestamp, unique_event_id: eventModel.unique_event_id }; }) ) ); } function getEcommerceAction(eventModel) { if (eventModel.ecommerce) { const actions = [ 'detail', 'click', 'add', 'remove', 'checkout', 'checkout_option', 'purchase', 'refund' ]; for (let index = 0; index < actions.length; ++index) { const action = actions[index]; if (eventModel.ecommerce[action]) { return action; } } } return null; } function setRedirectLocation() { let location = data.redirectTo; if (data.lookupForRedirectToParam && data.redirectToQueryParamName) { const param = getRequestQueryParameter(data.redirectToQueryParamName); if (param && param.startsWith('http')) { location = param; } } setResponseHeader('location', location); } function setClientErrorResponseMessage() { if (data.clientErrorResponseMessage) { setResponseBody(data.clientErrorResponseMessage); } } function getEventModels(baseEventModel) { const body = getRequestBody(); if (body) { const contentType = getRequestHeader('content-type'); const isFormUrlEncoded = !!contentType && contentType.indexOf('application/x-www-form-urlencoded') !== -1; let bodyJson = isFormUrlEncoded ? parseUrlEncoded(body) : JSON.parse(body); if (bodyJson) { const bodyType = getType(bodyJson); const shouldUseOriginalBody = data.acceptMultipleEvents && bodyType === 'array'; if (!shouldUseOriginalBody) { bodyJson = [bodyJson]; isEventModelsWrappedInArray = true; } return bodyJson.map((bodyItem) => { const eventModel = assign( { timestamp: makeInteger(getTimestampMillis() / 1000), unique_event_id: getTimestampMillis() + '_' + generateRandom(100000000, 999999999) }, baseEventModel ); for (let bodyItemKey in bodyItem) { eventModel[bodyItemKey] = bodyItem[bodyItemKey]; } return eventModel; }); } } return [ assign( { timestamp: makeInteger(getTimestampMillis() / 1000), unique_event_id: getTimestampMillis() + '_' + generateRandom(100000000, 999999999) }, baseEventModel ) ]; } function getClientId(eventModels) { for (let i = 0; i < eventModels.length; i++) { const eventModel = eventModels[i]; const clientId = eventModel.client_id || eventModel.data_client_id || eventModel._dcid; if (clientId) return clientId; } const dcid = getCookieValues('_dcid'); if (dcid && dcid[0]) return dcid[0]; if (data.generateClientId) { for (let i = 0; i < eventModels.length; i++) { const eventModel = eventModels[i]; const tempClientId = eventModel._dcid_temp; if (tempClientId) return tempClientId; } return ( 'dcid.1.' + getTimestampMillis() + '.' + generateRandom(100000000, 999999999) ); } return ''; } /*============================================================================== Helpers ==============================================================================*/ function getObjectLength(object) { let length = 0; for (let key in object) { if (object.hasOwnProperty(key)) { ++length; } } return length; } function assign() { const target = arguments[0]; for (let i = 1; i < arguments.length; i++) { for (let key in arguments[i]) { target[key] = arguments[i][key]; } } return target; } function parseUrlEncoded(data) { const pairs = data.split('&'); const parsedData = {}; const regex = createRegex('\\+', 'g'); for (const pair of pairs) { const pairValue = pair.split('='); const key = pairValue[0]; const value = pairValue[1]; const keys = key .split('.') .map((k) => decodeUriComponent(k.replace(regex, ' '))); let currentObject = parsedData; for (let i = 0; i < keys.length - 1; i++) { const currentKey = keys[i]; if (!currentObject[currentKey]) { const nextKey = keys[i + 1]; const nextKeyIsNumber = makeString(makeInteger(nextKey)) === nextKey; currentObject[currentKey] = nextKeyIsNumber ? [] : {}; } currentObject = currentObject[currentKey]; } const lastKey = keys[keys.length - 1]; const decodedValue = decodeUriComponent(value.replace(regex, ' ')); const parsedValue = JSON.parse(decodedValue) || decodedValue; if (getType(currentObject) === 'array') { currentObject.push(parsedValue); } else { currentObject[lastKey] = parsedValue; } } return parsedData; } ___SERVER_PERMISSIONS___ [ { "instance": { "key": { "publicId": "return_response", "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": "run_container", "versionId": "1" }, "param": [] }, "isRequired": true }, { "instance": { "key": { "publicId": "get_cookies", "versionId": "1" }, "param": [ { "key": "cookieAccess", "value": { "type": 1, "string": "specific" } }, { "key": "cookieNames", "value": { "type": 2, "listItem": [ { "type": 1, "string": "stape" }, { "type": 1, "string": "_dcid" }, { "type": 1, "string": "FPIDP" }, { "type": 1, "string": "FPID" } ] } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true }, { "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": "set_cookies", "versionId": "1" }, "param": [ { "key": "allowedCookies", "value": { "type": 2, "listItem": [ { "type": 3, "mapKey": [ { "type": 1, "string": "name" }, { "type": 1, "string": "domain" }, { "type": 1, "string": "path" }, { "type": 1, "string": "secure" }, { "type": 1, "string": "session" } ], "mapValue": [ { "type": 1, "string": "stape" }, { "type": 1, "string": "*" }, { "type": 1, "string": "*" }, { "type": 1, "string": "any" }, { "type": 1, "string": "any" } ] }, { "type": 3, "mapKey": [ { "type": 1, "string": "name" }, { "type": 1, "string": "domain" }, { "type": 1, "string": "path" }, { "type": 1, "string": "secure" }, { "type": 1, "string": "session" } ], "mapValue": [ { "type": 1, "string": "_dcid" }, { "type": 1, "string": "*" }, { "type": 1, "string": "*" }, { "type": 1, "string": "any" }, { "type": 1, "string": "any" } ] }, { "type": 3, "mapKey": [ { "type": 1, "string": "name" }, { "type": 1, "string": "domain" }, { "type": 1, "string": "path" }, { "type": 1, "string": "secure" }, { "type": 1, "string": "session" } ], "mapValue": [ { "type": 1, "string": "FPIDP" }, { "type": 1, "string": "*" }, { "type": 1, "string": "*" }, { "type": 1, "string": "any" }, { "type": 1, "string": "any" } ] } ] } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true } ] ___TESTS___ scenarios: - name: runContainer is called succesfully for a single event and response is sent back once code: "mock('getRequestPath', '/data');\nmock('requestMethod', 'POST');\nmock('getRequestBody',\ \ '{\"event\":\"page_view\",\"client_id\":\"client_id\"}');\n\n/* \n For some\ \ reason when we use \"assertApi('claimRequest').wasCalled()\" AND we run all\ \ the tests,\n it produces the following error \"Tried to claim a request after\ \ a Client had returned. Calling claimRequest from a callback is not supported.\"\ \n If we run only this single test, the error does not occur.\n A workaround\ \ is to mock 'claimRequest' API and make a dummy assertion in the mocked function.\ \ This way we make sure it's been called.\n*/\nlet claimRequestWasCalled;\nmock('claimRequest',\ \ () => {\n claimRequestWasCalled = true;\n});\n\nlet runContainerExecutions\ \ = 0;\nconst runContainerExpectedExecutions = 1;\nmock('runContainer', (eventData,\ \ onCompleteCallback, onStartCallback) => {\n assertThat(eventData).isObject();\n\ \ runContainerExecutions++;\n assertThat(onCompleteCallback).isFunction();\n\ \ onCompleteCallback();\n});\n\nmock('returnResponse', () => {\n if (runContainerExecutions\ \ !== runContainerExpectedExecutions) {\n fail('returnResponse should be called\ \ by the last runContainer onComplete callback execution only.');\n return;\n\ \ }\n assertThat(true).isTrue(); // Dummy to cover the case when runContainerExecutions\ \ === runContainerExpectedExecutions.\n});\n\nrunCode(mockData);\n\ncallLater(()\ \ => {\n assertThat(claimRequestWasCalled).isTrue();\n assertApi('setResponseStatus').wasCalledWith(200);\n\ \ assertThat(runContainerExecutions).isEqualTo(runContainerExpectedExecutions);\n\ });" - name: runContainer is called succesfully for multiple events and response is sent back once code: "mockData.acceptMultipleEvents = true;\n\nmock('getRequestPath', '/data');\n\ mock('requestMethod', 'POST');\nmock('getRequestBody', '[{\"event\":\"page_view\"\ ,\"client_id\":\"client_id\"},{\"event\":\"view_item\",\"client_id\":\"client_id\"\ }]');\n\n/* \n For some reason when we use \"assertApi('claimRequest').wasCalled()\"\ \ AND we run all the tests,\n it produces the following error \"Tried to claim\ \ a request after a Client had returned. Calling claimRequest from a callback\ \ is not supported.\"\n If we run only this single test, the error does not occur.\n\ \ A workaround is to mock 'claimRequest' API and make a dummy assertion in the\ \ mocked function. This way we make sure it's been called.\n*/\nlet claimRequestWasCalled;\n\ mock('claimRequest', () => {\n claimRequestWasCalled = true;\n});\n\nlet runContainerExecutions\ \ = 0;\nconst runContainerExpectedExecutions = 2;\nmock('runContainer', (eventData,\ \ onCompleteCallback, onStartCallback) => {\n assertThat(eventData).isObject();\n\ \ runContainerExecutions++;\n assertThat(onCompleteCallback).isFunction();\n\ \ onCompleteCallback();\n});\n\nmock('returnResponse', () => {\n if (runContainerExecutions\ \ !== runContainerExpectedExecutions) {\n fail('returnResponse should be called\ \ by the last runContainer onComplete callback execution only.');\n return;\n\ \ }\n assertThat(true).isTrue(); // Dummy to cover the case when runContainerExecutions\ \ === runContainerExpectedExecutions.\n});\n\nrunCode(mockData);\n\ncallLater(()\ \ => {\n assertThat(claimRequestWasCalled).isTrue();\n assertApi('setResponseStatus').wasCalledWith(200);\n\ \ assertThat(runContainerExecutions).isEqualTo(runContainerExpectedExecutions);\n\ });" - name: Should fallback to 200 if Response Status Code is absent from the data object code: "mockData.acceptMultipleEvents = true;\n\nObject.delete(mockData, 'responseStatusCode');\n\ \nmock('getRequestPath', '/data');\nmock('requestMethod', 'POST');\nmock('getRequestBody',\ \ '[{\"event\":\"page_view\",\"client_id\":\"client_id\"},{\"event\":\"view_item\"\ ,\"client_id\":\"client_id\"}]');\n\n/* \n For some reason when we use \"assertApi('claimRequest').wasCalled()\"\ \ AND we run all the tests,\n it produces the following error \"Tried to claim\ \ a request after a Client had returned. Calling claimRequest from a callback\ \ is not supported.\"\n If we run only this single test, the error does not occur.\n\ \ A workaround is to mock 'claimRequest' API and make a dummy assertion in the\ \ mocked function. This way we make sure it's been called.\n*/\nlet claimRequestWasCalled;\n\ mock('claimRequest', () => {\n claimRequestWasCalled = true;\n});\n\nmock('setResponseStatus',\ \ (responseStatus) => {\n assertThat(responseStatus).isEqualTo(200);\n});\n\n\ /*\n For some reason when we mock 'claimRequest', the following error is shown\ \ when the test is run: 'Request must be claimed before calling runContainer.'\n\ \ A workaround is to mock 'runContainer' and simply call its callbacks.\n*/\n\ mock('runContainer', (eventData, onCompleteCallback, onStartCallback) => {\n \ \ onCompleteCallback();\n});\n\nrunCode(mockData);\n\ncallLater(() => {\n assertThat(claimRequestWasCalled).isTrue();\n\ \ assertApi('returnResponse').wasCalled();\n});" - name: Client ID retrieval and generation (from _dcid cookie, from Event Data pre-defined keys and from Temporary Client ID) code: "const originalMockData = mockData;\n\nmock('getRequestPath', '/data');\n\ mock('requestMethod', 'POST');\n\n[\n {\n description: 'Should use Client\ \ ID from Event Model if one is present',\n mockData: {\n generateClientId:\ \ true,\n acceptMultipleEvents: true\n },\n mock: () => {\n mock('getRequestBody',\ \ '[{\"event\":\"page_view\",\"client_id\":\"client_id\",\"_dcid_temp\":\"_dcid_temp\"\ },{\"event\":\"view_item\",\"_dcid\":\"_dcid\",\"_dcid_temp\":\"_dcid_temp\"}]');\n\ \ mock('getCookieValues', []);\n },\n assert: (eventData) => {\n \ \ assertThat(eventData.client_id).isEqualTo('client_id'); // The first found\ \ Client ID is used for all events in the payload.\n }\n },\n {\n description:\ \ 'Should use Client ID from _dcid cookie if present and no Client ID is present\ \ in Event Model',\n mockData: {\n generateClientId: true,\n acceptMultipleEvents:\ \ true\n }, \n mock: () => {\n mock('getRequestBody', '[{\"event\"\ :\"page_view\",\"_dcid_temp\":\"_dcid_temp\"},{\"event\":\"view_item\",\"_dcid_temp\"\ :\"_dcid_temp\"}]');\n mock('getCookieValues', ['_dcid_from_cookie']);\n\ \ },\n assert: (eventData) => {\n assertThat(eventData.client_id).isEqualTo('_dcid_from_cookie');\n\ \ }\n },\n { \n description: 'Should use Client ID from temporary Client\ \ ID if option is enabled, and it is present in Event Model and, _dcid cookie\ \ or Client ID in Event Data do not exist',\n mockData: {\n generateClientId:\ \ true,\n acceptMultipleEvents: true\n },\n mock: () => {\n mock('getRequestBody',\ \ '[{\"event\":\"page_view\",\"_dcid_temp\":\"_dcid_temp\"},{\"event\":\"view_item\"\ ,\"_dcid_temp\":\"_dcid_temp\"}]');\n mock('getCookieValues', []);\n },\n\ \ assert: (eventData) => {\n assertThat(eventData.client_id).isEqualTo('_dcid_temp');\n\ \ }\n },\n { \n description: 'Should generate a random Client ID if option\ \ is enabled, and _dcid cookie or Client ID in Event Data or temporary Client\ \ ID do not exist',\n mockData: {\n generateClientId: true,\n acceptMultipleEvents:\ \ true\n },\n mock: () => {\n mock('getRequestBody', '[{\"event\":\"\ page_view\"},{\"event\":\"view_item\"}]');\n mock('getCookieValues', []);\n\ \ },\n assert: (eventData) => {\n assertThat(eventData.client_id).isEqualTo('dcid.1.1747945830456.123456789');\n\ \ }\n },\n { \n description: 'Should NOT generate a random Client ID or\ \ use temporary Client ID if option is disabled',\n mockData: {\n generateClientId:\ \ false,\n acceptMultipleEvents: true\n },\n mock: () => {\n mock('getRequestBody',\ \ '[{\"event\":\"page_view\"},{\"event\":\"view_item\"}]');\n mock('getCookieValues',\ \ []);\n },\n assert: (eventData) => {\n assertThat(eventData.client_id).isFalsy();\n\ \ }\n }\n].forEach(scenario => {\n const copyMockData = JSON.parse(JSON.stringify(originalMockData));\n\ \ mergeObj(copyMockData, scenario.mockData);\n \n scenario.mock();\n \n /*\ \ \n For some reason when we use \"assertApi('claimRequest').wasCalled()\"\ \ AND we run all the tests,\n it produces the following error \"Tried to claim\ \ a request after a Client had returned. Calling claimRequest from a callback\ \ is not supported.\"\n If we run only this single test, the error does not\ \ occur.\n A workaround is to mock 'claimRequest' API and make a dummy assertion\ \ in the mocked function. This way we make sure it's been called.\n */\n let\ \ claimRequestWasCalled;\n mock('claimRequest', () => {\n claimRequestWasCalled\ \ = true;\n });\n \n mock('setResponseStatus', (responseStatus) => {\n assertThat(responseStatus).isEqualTo(200);\n\ \ });\n \n /*\n For some reason when we mock 'claimRequest', the following\ \ error is shown when the test is run: 'Request must be claimed before calling\ \ runContainer.'\n A workaround is to mock 'runContainer' and simply call its\ \ callbacks.\n */\n mock('runContainer', (eventData, onCompleteCallback, onStartCallback)\ \ => {\n assertThat(eventData.hasOwnProperty('_temp_dcid')).isFalse();\n \ \ scenario.assert(eventData);\n onCompleteCallback(); \n });\n \n runCode(copyMockData);\n\ \ \n callLater(() => {\n assertThat(claimRequestWasCalled).isTrue();\n \ \ assertApi('returnResponse').wasCalled();\n });\n});" setup: |- const JSON = require('JSON'); const Object = require('Object'); const callLater = require('callLater'); function mergeObj(target, source) { for (const key in source) { if (source.hasOwnProperty(key)) target[key] = source[key]; } return target; } const mockData = { responseStatusCode: 200, generateClientId: true, responseBody: 'timestamp', prolongCookies: true, httpOnlyCookie: false, responseBodyGet: false, acceptMultipleEvents: false, exposeFPIDCookie: false }; mock('getRequestHeader', (header) => { if (header === 'trace-id') return 'expectedTraceId'; }); mock('getTimestampMillis', 1747945830456); mock('generateRandom', 123456789); ___NOTES___ Created on 21/03/2021, 11:24:30