├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── metadata.yaml ├── template.js └── template.tpl /.gitignore: -------------------------------------------------------------------------------- 1 | src/ 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Stape 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Data Client for Google Tag Manager Server Side 2 | 3 | Data Client developed for receiving requests from [Data Tag](https://github.com/stape-io/data-tag) placed inside the Google Tag Manager Web Container, but it can also receive any request and map it to the Event Data inside the Google Tag Manager Server Side container. 4 | 5 | ## Useful resources 6 | 7 | - https://stape.io/solutions/data-tag-client 8 | - https://stape.io/blog/sending-data-from-google-tag-manager-web-container-to-the-server-container 9 | - https://stape.io/blog/send-datalayer-push-from-server-gtm-to-web-gtm 10 | 11 | ## Open Source 12 | 13 | Data Client for Google Tag Manager Server Side is developed and maintained by [Stape Team](https://stape.io/) under the Apache 2.0 license. 14 | -------------------------------------------------------------------------------- /metadata.yaml: -------------------------------------------------------------------------------- 1 | homepage: "https://stape.io/" 2 | versions: 3 | - sha: 953bf24c77b68e708fee1e953eea31303bf1fde1 4 | changeNotes: Fix for timestamp and unique_event_id override. 5 | - sha: a9fb729fd9eed2013188c00e820fa81aa78e71c1 6 | changeNotes: Fixed location response header bug. 7 | - sha: e31d2f39c10285cbc5169e349c473f73e4668692 8 | changeNotes: Added support for x-www-form-urlencoded. 9 | - sha: 4d9f4d4fbded5414cb091efac70224d31c522e5c 10 | changeNotes: Fix user address data. 11 | - sha: 905af50fe8eb5f61e58a6d04c33fd6faf01e7f98 12 | changeNotes: user_data persistence fix. 13 | - sha: e11b7b68f8160fd6cc6d210daef907e25b4670cf 14 | changeNotes: Fix issue with gAds user data. 15 | - sha: f6511e191159a89583452d7ff2e97700069cec1c 16 | changeNotes: Add Matomo support. 17 | - sha: 199507c3b10f3acb51c6ce307129792fb07d9700 18 | changeNotes: Fix issue with rights. 19 | - sha: cb233b88ee98f576d2d27fa4c06234cecb66b268 20 | changeNotes: Accept multiple events. 21 | - sha: 42a2c80254ef261b4c79e4414ff4525a239856af 22 | changeNotes: Allow to configure custom redirects. 23 | - sha: 448e609ee33eedbfaa2070d70eb23b8776f95fe3 24 | changeNotes: Add unique_event_id to eventData. 25 | - sha: 91da5a8f79c50216ecd882a1e7a478520b418e37 26 | changeNotes: Update EE parsing. 27 | - sha: 65143e70107a90b10297d080d342b7056a74eefd 28 | changeNotes: Add additional data for EE events. 29 | - sha: fb78186e3fa6f49bde7a32b9e4714eaf4d1cce86 30 | changeNotes: Add response settings. 31 | - sha: 7ff22a33ef4eb76b0311dd45cd40e92ab7068888 32 | changeNotes: Update cookie storing. 33 | - sha: f0e02106d0c538455c4b959051066fbb59f7afa0 34 | changeNotes: Add Data Tag dtcd parameter support. 35 | - sha: a88f51afe94db1f5e5a97ebe5cf251fc0cb444c9 36 | changeNotes: Add client_id generation functionality. 37 | - sha: 28b7b5dc4dfca58555fd15fff3fefe18b7b49762 38 | changeNotes: Add data store functionality. 39 | - sha: 713fbe32cc954debebd9f145de8c9224ce75e110 40 | changeNotes: Add FPIDP cookie. 41 | - sha: 3bc72ee6f317905f2356e452014ac88f42336c03 42 | changeNotes: Move from gtm-server to stape. 43 | - sha: 5ae9e0c010cf4e4c0c4d1dcc70fac5c28dbe1a66 44 | changeNotes: Do not run container on option request. 45 | - sha: 7c3b0f7ff92880ce3a83dbf5f4eca315e468e2a4 46 | changeNotes: Move transformation logic to the web. 47 | - sha: e927adb13b807eb375c07553da418f592be51e1e 48 | changeNotes: Initial release. 49 | -------------------------------------------------------------------------------- /template.js: -------------------------------------------------------------------------------- 1 | const returnResponse = require('returnResponse'); 2 | const runContainer = require('runContainer'); 3 | const setResponseHeader = require('setResponseHeader'); 4 | const setResponseStatus = require('setResponseStatus'); 5 | const setResponseBody = require('setResponseBody'); 6 | const JSON = require('JSON'); 7 | const fromBase64 = require('fromBase64'); 8 | const getTimestampMillis = require('getTimestampMillis'); 9 | const getCookieValues = require('getCookieValues'); 10 | const getRequestBody = require('getRequestBody'); 11 | const getRequestMethod = require('getRequestMethod'); 12 | const getRequestHeader = require('getRequestHeader'); 13 | const getRequestPath = require('getRequestPath'); 14 | const getRequestQueryParameters = require('getRequestQueryParameters'); 15 | const makeInteger = require('makeInteger'); 16 | const getRemoteAddress = require('getRemoteAddress'); 17 | const setCookie = require('setCookie'); 18 | const setPixelResponse = require('setPixelResponse'); 19 | const generateRandom = require('generateRandom'); 20 | const computeEffectiveTldPlusOne = require('computeEffectiveTldPlusOne'); 21 | const getRequestQueryParameter = require('getRequestQueryParameter'); 22 | const getType = require('getType'); 23 | const Promise = require('Promise'); 24 | const decodeUriComponent = require('decodeUriComponent'); 25 | const createRegex = require('createRegex'); 26 | const makeString = require('makeString'); 27 | 28 | const requestMethod = getRequestMethod(); 29 | const path = getRequestPath(); 30 | let isClientUsed = false; 31 | let isEventModelsWrappedInArray = false; 32 | 33 | if (path === '/data') { 34 | runClient(); 35 | } 36 | 37 | if (data.path && !isClientUsed) { 38 | for (let key in data.path) { 39 | if (!isClientUsed && data.path[key].path === path) { 40 | runClient(); 41 | } 42 | } 43 | } 44 | 45 | function runClient() { 46 | isClientUsed = true; 47 | require('claimRequest')(); 48 | 49 | if (requestMethod === 'OPTIONS') { 50 | setCommonResponseHeaders(200); 51 | returnResponse(); 52 | return; 53 | } 54 | const baseEventModel = getBaseEventModelWithQueryParameters(); 55 | let eventModels = getEventModels(baseEventModel); 56 | const clientId = getClientId(eventModels); 57 | eventModels = eventModels.map((eventModel) => { 58 | eventModel = addRequiredParametersToEventModel(eventModel); 59 | eventModel = addCommonParametersToEventModel(eventModel); 60 | eventModel = addClientIdToEventModel(eventModel, clientId); 61 | return eventModel; 62 | }); 63 | 64 | storeClientId(eventModels[0]); 65 | exposeFPIDCookie(eventModels[0]); 66 | prolongDataTagCookies(eventModels[0]); 67 | const responseStatusCode = makeInteger(data.responseStatusCode); 68 | setCommonResponseHeaders(responseStatusCode); 69 | 70 | Promise.all( 71 | eventModels.map((eventModel) => { 72 | return Promise.create((resolve) => { 73 | runContainer(eventModel, resolve); 74 | }); 75 | }) 76 | ).then(() => { 77 | switch (responseStatusCode) { 78 | case 200: 79 | case 201: 80 | if (requestMethod === 'POST' || data.responseBodyGet) { 81 | prepareResponseBody(eventModels); 82 | } else { 83 | setPixelResponse(); 84 | } 85 | break; 86 | case 301: 87 | case 302: 88 | setRedirectLocation(); 89 | break; 90 | case 403: 91 | case 404: 92 | setClientErrorResponseMessage(); 93 | break; 94 | } 95 | 96 | returnResponse(); 97 | }); 98 | } 99 | 100 | function addCommonParametersToEventModel(eventModel) { 101 | if (!eventModel.ip_override) { 102 | if (eventModel.ip) eventModel.ip_override = eventModel.ip; 103 | else if (eventModel.ipOverride) 104 | eventModel.ip_override = eventModel.ipOverride; 105 | else eventModel.ip_override = getRemoteAddress(); 106 | } 107 | 108 | if (!eventModel.user_agent) { 109 | if (eventModel.userAgent) eventModel.user_agent = eventModel.userAgent; 110 | else if (getRequestHeader('User-Agent')) 111 | eventModel.user_agent = getRequestHeader('User-Agent'); 112 | } 113 | 114 | if (!eventModel.language) { 115 | const acceptLanguageHeader = getRequestHeader('Accept-Language'); 116 | 117 | if (acceptLanguageHeader) { 118 | eventModel.language = acceptLanguageHeader 119 | .split(';')[0] 120 | .substring(0, 2) 121 | .toLowerCase(); 122 | } 123 | } 124 | 125 | if (!eventModel.page_hostname) { 126 | if (eventModel.pageHostname) 127 | eventModel.page_hostname = eventModel.pageHostname; 128 | else if (eventModel.hostname) 129 | eventModel.page_hostname = eventModel.hostname; 130 | } 131 | 132 | if (!eventModel.page_location) { 133 | if (eventModel.pageLocation) 134 | eventModel.page_location = eventModel.pageLocation; 135 | else if (eventModel.url) eventModel.page_location = eventModel.url; 136 | else if (eventModel.href) eventModel.page_location = eventModel.href; 137 | } 138 | 139 | if (!eventModel.page_referrer) { 140 | if (eventModel.pageReferrer) 141 | eventModel.page_referrer = eventModel.pageReferrer; 142 | else if (eventModel.referrer) 143 | eventModel.page_referrer = eventModel.referrer; 144 | else if (eventModel.urlref) eventModel.page_referrer = eventModel.urlref; 145 | } 146 | 147 | if (!eventModel.value && eventModel.e_v) eventModel.value = eventModel.e_v; 148 | 149 | if (getType(eventModel.items) === 'array' && eventModel.items.length) { 150 | const firstItem = eventModel.items[0]; 151 | if (!eventModel.currency && firstItem.currency) 152 | eventModel.currency = firstItem.currency; 153 | if (eventModel.items.length === 1) { 154 | if (!eventModel.item_id && firstItem.item_id) 155 | eventModel.item_id = firstItem.item_id; 156 | if (!eventModel.item_name && firstItem.item_name) 157 | eventModel.item_name = firstItem.item_name; 158 | if (!eventModel.item_brand && firstItem.item_brand) 159 | eventModel.item_brand = firstItem.item_brand; 160 | if (!eventModel.item_quantity && firstItem.quantity) 161 | eventModel.item_quantity = firstItem.quantity; 162 | if (!eventModel.item_category && firstItem.item_category) 163 | eventModel.item_category = firstItem.item_category; 164 | if (!eventModel.item_price && firstItem.price) 165 | eventModel.item_price = firstItem.price; 166 | } 167 | if (!eventModel.value) { 168 | const valueFromItems = eventModel.items.reduce((acc, item) => { 169 | if (!item.price) return acc; 170 | const quantity = item.quantity ? item.quantity : 1; 171 | return acc + quantity * item.price; 172 | }, 0); 173 | if (valueFromItems) eventModel.value = valueFromItems; 174 | } 175 | } 176 | 177 | const ecommerceAction = getEcommerceAction(eventModel); 178 | 179 | if (ecommerceAction) { 180 | if (!eventModel['x-ga-mp1-pa']) eventModel['x-ga-mp1-pa'] = ecommerceAction; 181 | 182 | if ( 183 | ecommerceAction === 'purchase' && 184 | eventModel.ecommerce.purchase.actionField 185 | ) { 186 | if (!eventModel['x-ga-mp1-tr']) 187 | eventModel['x-ga-mp1-tr'] = 188 | eventModel.ecommerce.purchase.actionField.revenue; 189 | if (!eventModel.revenue) 190 | eventModel.revenue = eventModel.ecommerce.purchase.actionField.revenue; 191 | if (!eventModel.affiliation) 192 | eventModel.affiliation = 193 | eventModel.ecommerce.purchase.actionField.affiliation; 194 | if (!eventModel.tax) 195 | eventModel.tax = eventModel.ecommerce.purchase.actionField.tax; 196 | if (!eventModel.shipping) 197 | eventModel.shipping = 198 | eventModel.ecommerce.purchase.actionField.shipping; 199 | if (!eventModel.coupon) 200 | eventModel.coupon = eventModel.ecommerce.purchase.actionField.coupon; 201 | if (!eventModel.transaction_id) 202 | eventModel.transaction_id = 203 | eventModel.ecommerce.purchase.actionField.id; 204 | } 205 | } 206 | 207 | if (!eventModel.page_encoding && eventModel.pageEncoding) 208 | eventModel.page_encoding = eventModel.pageEncoding; 209 | if (!eventModel.page_path && eventModel.pagePath) 210 | eventModel.page_path = eventModel.pagePath; 211 | if (!eventModel.page_title && eventModel.pageTitle) 212 | eventModel.page_title = eventModel.pageTitle; 213 | if (!eventModel.screen_resolution && eventModel.screenResolution) 214 | eventModel.screen_resolution = eventModel.screenResolution; 215 | if (!eventModel.viewport_size && eventModel.viewportSize) 216 | eventModel.viewport_size = eventModel.viewportSize; 217 | if (!eventModel.user_id && eventModel.userId) 218 | eventModel.user_id = eventModel.userId; 219 | 220 | if (!eventModel.user_data) { 221 | let userData = {}; 222 | let userAddressData = {}; 223 | 224 | if (!userData.email_address) { 225 | if (eventModel.userEmail) userData.email_address = eventModel.userEmail; 226 | else if (eventModel.email_address) 227 | userData.email_address = eventModel.email_address; 228 | else if (eventModel.email) userData.email_address = eventModel.email; 229 | else if (eventModel.mail) userData.email_address = eventModel.mail; 230 | } 231 | 232 | if (!userData.phone_number) { 233 | if (eventModel.userPhoneNumber) 234 | userData.phone_number = eventModel.userPhoneNumber; 235 | else if (eventModel.phone_number) 236 | userData.phone_number = eventModel.phone_number; 237 | else if (eventModel.phoneNumber) 238 | userData.phone_number = eventModel.phoneNumber; 239 | else if (eventModel.phone) userData.phone_number = eventModel.phone; 240 | } 241 | 242 | if (!userAddressData.street && eventModel.street) 243 | userAddressData.street = eventModel.street; 244 | if (!userAddressData.city && eventModel.city) 245 | userAddressData.city = eventModel.city; 246 | if (!userAddressData.region && eventModel.region) 247 | userAddressData.region = eventModel.region; 248 | if (!userAddressData.country && eventModel.country) 249 | userAddressData.country = eventModel.country; 250 | 251 | if (!userAddressData.first_name) { 252 | if (eventModel.userFirstName) 253 | userAddressData.first_name = eventModel.userFirstName; 254 | else if (eventModel.first_name) 255 | userAddressData.first_name = eventModel.first_name; 256 | else if (eventModel.firstName) 257 | userAddressData.first_name = eventModel.firstName; 258 | else if (eventModel.name) userAddressData.first_name = eventModel.name; 259 | } 260 | 261 | if (!userAddressData.last_name) { 262 | if (eventModel.userLastName) 263 | userAddressData.last_name = eventModel.userLastName; 264 | else if (eventModel.last_name) 265 | userAddressData.last_name = eventModel.last_name; 266 | else if (eventModel.lastName) 267 | userAddressData.last_name = eventModel.lastName; 268 | else if (eventModel.surname) 269 | userAddressData.last_name = eventModel.surname; 270 | else if (eventModel.family_name) 271 | userAddressData.last_name = eventModel.family_name; 272 | else if (eventModel.familyName) 273 | userAddressData.last_name = eventModel.familyName; 274 | } 275 | 276 | if (!userAddressData.region) { 277 | if (eventModel.region) userAddressData.region = eventModel.region; 278 | else if (eventModel.state) userAddressData.region = eventModel.state; 279 | } 280 | 281 | if (!userAddressData.postal_code) { 282 | if (eventModel.postal_code) 283 | userAddressData.postal_code = eventModel.postal_code; 284 | else if (eventModel.postalCode) 285 | userAddressData.postal_code = eventModel.postalCode; 286 | else if (eventModel.zip) userAddressData.postal_code = eventModel.zip; 287 | } 288 | 289 | if (getObjectLength(userAddressData) !== 0) { 290 | userData.address = userAddressData; 291 | } 292 | 293 | if (!eventModel.user_data && getObjectLength(userData) !== 0) { 294 | eventModel.user_data = userData; 295 | } 296 | } 297 | 298 | return eventModel; 299 | } 300 | 301 | function getBaseEventModelWithQueryParameters() { 302 | const requestQueryParameters = getRequestQueryParameters(); 303 | const eventModel = {}; 304 | 305 | if (requestQueryParameters) { 306 | for (let queryParameterKey in requestQueryParameters) { 307 | if ( 308 | (queryParameterKey === 'dtcd' || queryParameterKey === 'dtdc') && 309 | requestMethod === 'GET' 310 | ) { 311 | let dt = 312 | queryParameterKey === 'dtcd' 313 | ? JSON.parse(requestQueryParameters[queryParameterKey]) 314 | : JSON.parse(fromBase64(requestQueryParameters[queryParameterKey])); 315 | 316 | for (let dtKey in dt) { 317 | eventModel[dtKey] = dt[dtKey]; 318 | } 319 | } else { 320 | eventModel[queryParameterKey] = 321 | requestQueryParameters[queryParameterKey]; 322 | } 323 | } 324 | } 325 | 326 | return eventModel; 327 | } 328 | 329 | function addClientIdToEventModel(eventModel, clientId) { 330 | eventModel.client_id = clientId; 331 | return eventModel; 332 | } 333 | 334 | function prolongDataTagCookies(eventModel) { 335 | if (data.prolongCookies) { 336 | let stapeData = getCookieValues('stape'); 337 | 338 | if (stapeData.length) { 339 | setCookie('stape', stapeData[0], { 340 | domain: 'auto', 341 | path: '/', 342 | samesite: getCookieType(eventModel), 343 | secure: true, 344 | 'max-age': 63072000, // 2 years 345 | httpOnly: false 346 | }); 347 | } 348 | } 349 | } 350 | 351 | function addRequiredParametersToEventModel(eventModel) { 352 | if (!eventModel.event_name) { 353 | let eventName = 'Data'; 354 | 355 | if (eventModel.eventName) eventName = eventModel.eventName; 356 | else if (eventModel.event) eventName = eventModel.event; 357 | else if (eventModel.e_n) eventName = eventModel.e_n; 358 | 359 | eventModel.event_name = eventName; 360 | } 361 | 362 | return eventModel; 363 | } 364 | 365 | function exposeFPIDCookie(eventModel) { 366 | if (data.exposeFPIDCookie) { 367 | let fpid = getCookieValues('FPID'); 368 | 369 | if (fpid.length) { 370 | setCookie('FPIDP', fpid[0], { 371 | domain: 'auto', 372 | path: '/', 373 | samesite: getCookieType(eventModel), 374 | secure: true, 375 | 'max-age': 63072000, // 2 years 376 | httpOnly: false 377 | }); 378 | } 379 | } 380 | } 381 | 382 | function storeClientId(eventModel) { 383 | if (data.generateClientId) { 384 | setCookie('_dcid', eventModel.client_id, { 385 | domain: 'auto', 386 | path: '/', 387 | samesite: getCookieType(eventModel), 388 | secure: true, 389 | 'max-age': 63072000, // 2 years 390 | httpOnly: data.httpOnlyCookie || false 391 | }); 392 | } 393 | } 394 | 395 | function getObjectLength(object) { 396 | let length = 0; 397 | 398 | for (let key in object) { 399 | if (object.hasOwnProperty(key)) { 400 | ++length; 401 | } 402 | } 403 | return length; 404 | } 405 | 406 | function setCommonResponseHeaders(statusCode) { 407 | setResponseHeader('Access-Control-Max-Age', '600'); 408 | setResponseHeader('Access-Control-Allow-Origin', getRequestHeader('origin')); 409 | setResponseHeader( 410 | 'Access-Control-Allow-Methods', 411 | 'GET,POST,PUT,DELETE,OPTIONS' 412 | ); 413 | setResponseHeader( 414 | 'Access-Control-Allow-Headers', 415 | 'content-type,set-cookie,x-robots-tag,x-gtm-server-preview,x-stape-preview' 416 | ); 417 | setResponseHeader('Access-Control-Allow-Credentials', 'true'); 418 | setResponseStatus(statusCode); 419 | } 420 | 421 | function getCookieType(eventModel) { 422 | if (!eventModel.page_location) { 423 | return 'Lax'; 424 | } 425 | 426 | const host = getRequestHeader('host'); 427 | const effectiveTldPlusOne = computeEffectiveTldPlusOne( 428 | eventModel.page_location 429 | ); 430 | 431 | if (!host || !effectiveTldPlusOne) { 432 | return 'Lax'; 433 | } 434 | 435 | if (host && host.indexOf(effectiveTldPlusOne) !== -1) { 436 | return 'Lax'; 437 | } 438 | 439 | return 'None'; 440 | } 441 | 442 | function prepareResponseBody(eventModels) { 443 | if (data.responseBody === 'empty') { 444 | return; 445 | } 446 | 447 | const responseModel = isEventModelsWrappedInArray 448 | ? eventModels[0] 449 | : eventModels; 450 | 451 | setResponseHeader('Content-Type', 'application/json'); 452 | 453 | if (data.responseBody === 'eventData') { 454 | setResponseBody(JSON.stringify(responseModel)); 455 | 456 | return; 457 | } 458 | 459 | if (isEventModelsWrappedInArray) { 460 | setResponseBody( 461 | JSON.stringify({ 462 | timestamp: responseModel.timestamp, 463 | unique_event_id: responseModel.unique_event_id 464 | }) 465 | ); 466 | return; 467 | } 468 | 469 | setResponseBody( 470 | JSON.stringify( 471 | eventModels.map((eventModel) => { 472 | return { 473 | timestamp: eventModel.timestamp, 474 | unique_event_id: eventModel.unique_event_id 475 | }; 476 | }) 477 | ) 478 | ); 479 | } 480 | 481 | function getEcommerceAction(eventModel) { 482 | if (eventModel.ecommerce) { 483 | const actions = [ 484 | 'detail', 485 | 'click', 486 | 'add', 487 | 'remove', 488 | 'checkout', 489 | 'checkout_option', 490 | 'purchase', 491 | 'refund' 492 | ]; 493 | 494 | for (let index = 0; index < actions.length; ++index) { 495 | const action = actions[index]; 496 | 497 | if (eventModel.ecommerce[action]) { 498 | return action; 499 | } 500 | } 501 | } 502 | 503 | return null; 504 | } 505 | 506 | function setRedirectLocation() { 507 | let location = data.redirectTo; 508 | if (data.lookupForRedirectToParam && data.redirectToQueryParamName) { 509 | const param = getRequestQueryParameter(data.redirectToQueryParamName); 510 | if (param && param.startsWith('http')) { 511 | location = param; 512 | } 513 | } 514 | setResponseHeader('location', location); 515 | } 516 | 517 | function setClientErrorResponseMessage() { 518 | if (data.clientErrorResponseMessage) { 519 | setResponseBody(data.clientErrorResponseMessage); 520 | } 521 | } 522 | 523 | function getEventModels(baseEventModel) { 524 | const body = getRequestBody(); 525 | 526 | if (body) { 527 | const contentType = getRequestHeader('content-type'); 528 | const isFormUrlEncoded = 529 | !!contentType && 530 | contentType.indexOf('application/x-www-form-urlencoded') !== -1; 531 | let bodyJson = isFormUrlEncoded ? parseUrlEncoded(body) : JSON.parse(body); 532 | if (bodyJson) { 533 | const bodyType = getType(bodyJson); 534 | const shouldUseOriginalBody = 535 | data.acceptMultipleEvents && bodyType === 'array'; 536 | if (!shouldUseOriginalBody) { 537 | bodyJson = [bodyJson]; 538 | isEventModelsWrappedInArray = true; 539 | } 540 | 541 | return bodyJson.map((bodyItem) => { 542 | const eventModel = assign( 543 | { 544 | timestamp: makeInteger(getTimestampMillis() / 1000), 545 | unique_event_id: 546 | getTimestampMillis() + '_' + generateRandom(100000000, 999999999) 547 | }, 548 | baseEventModel 549 | ); 550 | for (let bodyItemKey in bodyItem) { 551 | eventModel[bodyItemKey] = bodyItem[bodyItemKey]; 552 | } 553 | return eventModel; 554 | }); 555 | } 556 | } 557 | 558 | return [ 559 | assign( 560 | { 561 | timestamp: makeInteger(getTimestampMillis() / 1000), 562 | unique_event_id: 563 | getTimestampMillis() + '_' + generateRandom(100000000, 999999999) 564 | }, 565 | baseEventModel 566 | ) 567 | ]; 568 | } 569 | 570 | function getClientId(eventModels) { 571 | for (let i = 0; i < eventModels.length; i++) { 572 | const eventModel = eventModels[i]; 573 | const clientId = 574 | eventModel.client_id || eventModel.data_client_id || eventModel._dcid; 575 | if (clientId) return clientId; 576 | } 577 | 578 | const dcid = getCookieValues('_dcid'); 579 | if (dcid && dcid[0]) return dcid[0]; 580 | 581 | if (data.generateClientId) { 582 | return ( 583 | 'dcid.1.' + 584 | getTimestampMillis() + 585 | '.' + 586 | generateRandom(100000000, 999999999) 587 | ); 588 | } 589 | return ''; 590 | } 591 | 592 | function assign() { 593 | const target = arguments[0]; 594 | for (let i = 1; i < arguments.length; i++) { 595 | for (let key in arguments[i]) { 596 | target[key] = arguments[i][key]; 597 | } 598 | } 599 | return target; 600 | } 601 | 602 | function parseUrlEncoded(data) { 603 | const pairs = data.split('&'); 604 | const parsedData = {}; 605 | const regex = createRegex('\\+', 'g'); 606 | for (const pair of pairs) { 607 | const pairValue = pair.split('='); 608 | const key = pairValue[0]; 609 | const value = pairValue[1]; 610 | const keys = key 611 | .split('.') 612 | .map((k) => decodeUriComponent(k.replace(regex, ' '))); 613 | 614 | let currentObject = parsedData; 615 | 616 | for (let i = 0; i < keys.length - 1; i++) { 617 | const currentKey = keys[i]; 618 | 619 | if (!currentObject[currentKey]) { 620 | const nextKey = keys[i + 1]; 621 | const nextKeyIsNumber = makeString(makeInteger(nextKey)) === nextKey; 622 | currentObject[currentKey] = nextKeyIsNumber ? [] : {}; 623 | } 624 | 625 | currentObject = currentObject[currentKey]; 626 | } 627 | 628 | const lastKey = keys[keys.length - 1]; 629 | const decodedValue = decodeUriComponent(value.replace(regex, ' ')); 630 | const parsedValue = JSON.parse(decodedValue) || decodedValue; 631 | 632 | if (getType(currentObject) === 'array') { 633 | currentObject.push(parsedValue); 634 | } else { 635 | currentObject[lastKey] = parsedValue; 636 | } 637 | } 638 | 639 | return parsedData; 640 | } 641 | -------------------------------------------------------------------------------- /template.tpl: -------------------------------------------------------------------------------- 1 | ___TERMS_OF_SERVICE___ 2 | 3 | By creating or modifying this file you agree to Google Tag Manager's Community 4 | Template Gallery Developer Terms of Service available at 5 | https://developers.google.com/tag-manager/gallery-tos (or such other URL as 6 | Google may provide), as modified from time to time. 7 | 8 | 9 | ___INFO___ 10 | 11 | { 12 | "type": "CLIENT", 13 | "id": "cvt_temp_public_id", 14 | "version": 1, 15 | "securityGroups": [], 16 | "displayName": "Data Client", 17 | "brand": { 18 | "id": "brand_dummy", 19 | "displayName": "Stape", 20 | "thumbnail": "data:image/png;base64,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" 21 | }, 22 | "description": "Use this Client as a mapper from Request Data to Event Data.", 23 | "containerContexts": [ 24 | "SERVER" 25 | ] 26 | } 27 | 28 | 29 | ___TEMPLATE_PARAMETERS___ 30 | 31 | [ 32 | { 33 | "type": "CHECKBOX", 34 | "name": "exposeFPIDCookie", 35 | "checkboxText": "Expose FPID Cookie", 36 | "simpleValueType": true, 37 | "help": "If enabled, the server only accessible FPID cookie, generated by UA/GA4 client, will be duplicated to FPIDP cookie, which will be accessible from the client JS. Highly recommend using this option only in case it is necessary." 38 | }, 39 | { 40 | "type": "CHECKBOX", 41 | "name": "httpOnlyCookie", 42 | "checkboxText": "Write the _dcid cookie as HttpOnly", 43 | "simpleValueType": true, 44 | "help": "If enabled, the _dcid cookie will be written with the HttpOnly flag, making it non-accsessible by javascript.", 45 | "defaultValue": false 46 | }, 47 | { 48 | "type": "CHECKBOX", 49 | "name": "generateClientId", 50 | "checkboxText": "Always generate client_id parameter", 51 | "simpleValueType": true, 52 | "help": "If enabled, even if the `client_id` parameter will not be determined from the request, it will still be generated. The `client_id` parameter is required by UA/GA4 tags.", 53 | "defaultValue": true 54 | }, 55 | { 56 | "type": "CHECKBOX", 57 | "name": "prolongCookies", 58 | "checkboxText": "Automatically prolong Data Tag cookies", 59 | "simpleValueType": true, 60 | "help": "If enabled, cookies generated by Data tag will be reseated from the server with an expiration time of two years. Useful if you use Data tag store functionality.", 61 | "defaultValue": true 62 | }, 63 | { 64 | "type": "CHECKBOX", 65 | "name": "acceptMultipleEvents", 66 | "checkboxText": "Accept Multiple Events", 67 | "simpleValueType": true, 68 | "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.\nExample:\n\u003cbr /\u003e\n[\n {\"event\":\"page_view\"},\n {\"event\":\"view_item\"}\n]", 69 | "defaultValue": false 70 | }, 71 | { 72 | "type": "GROUP", 73 | "name": "responseSettings", 74 | "displayName": "Response Settings", 75 | "groupStyle": "ZIPPY_CLOSED", 76 | "subParams": [ 77 | { 78 | "type": "SELECT", 79 | "name": "responseStatusCode", 80 | "displayName": "Response Status Code", 81 | "selectItems": [ 82 | { 83 | "value": 200, 84 | "displayValue": "200" 85 | }, 86 | { 87 | "value": 201, 88 | "displayValue": "201" 89 | }, 90 | { 91 | "value": 301, 92 | "displayValue": "301" 93 | }, 94 | { 95 | "value": 302, 96 | "displayValue": "302" 97 | }, 98 | { 99 | "value": 403, 100 | "displayValue": "403" 101 | }, 102 | { 103 | "value": 404, 104 | "displayValue": "404" 105 | } 106 | ], 107 | "simpleValueType": true, 108 | "defaultValue": 200 109 | }, 110 | { 111 | "type": "GROUP", 112 | "name": "regularResponseSettings", 113 | "groupStyle": "NO_ZIPPY", 114 | "subParams": [ 115 | { 116 | "type": "SELECT", 117 | "name": "responseBody", 118 | "displayName": "Response Body", 119 | "macrosInSelect": false, 120 | "selectItems": [ 121 | { 122 | "value": "timestamp", 123 | "displayValue": "JSON Object with timestamp (recommended)" 124 | }, 125 | { 126 | "value": "eventData", 127 | "displayValue": "JSON Object with Event Data" 128 | }, 129 | { 130 | "value": "empty", 131 | "displayValue": "Empty" 132 | } 133 | ], 134 | "simpleValueType": true, 135 | "defaultValue": "timestamp" 136 | }, 137 | { 138 | "type": "CHECKBOX", 139 | "name": "responseBodyGet", 140 | "checkboxText": "Send Response Body for GET request", 141 | "simpleValueType": true, 142 | "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." 143 | } 144 | ], 145 | "enablingConditions": [ 146 | { 147 | "paramName": "responseStatusCode", 148 | "paramValue": 200, 149 | "type": "EQUALS" 150 | }, 151 | { 152 | "paramName": "responseStatusCode", 153 | "paramValue": 201, 154 | "type": "EQUALS" 155 | } 156 | ] 157 | }, 158 | { 159 | "type": "GROUP", 160 | "name": "redirectResponseSettings", 161 | "groupStyle": "NO_ZIPPY", 162 | "subParams": [ 163 | { 164 | "type": "TEXT", 165 | "name": "redirectTo", 166 | "displayName": "Redirect To", 167 | "simpleValueType": true, 168 | "valueValidators": [ 169 | { 170 | "type": "NON_EMPTY" 171 | }, 172 | { 173 | "type": "REGEX", 174 | "args": [ 175 | "^https?:\\/\\/.*" 176 | ] 177 | } 178 | ] 179 | }, 180 | { 181 | "type": "CHECKBOX", 182 | "name": "lookupForRedirectToParam", 183 | "checkboxText": "Try to find redirect destination in query params", 184 | "simpleValueType": true, 185 | "help": "Override destination URL with query param from request url" 186 | }, 187 | { 188 | "type": "TEXT", 189 | "name": "redirectToQueryParamName", 190 | "displayName": "Query Param Name", 191 | "simpleValueType": true, 192 | "enablingConditions": [ 193 | { 194 | "paramName": "lookupForRedirectToParam", 195 | "paramValue": true, 196 | "type": "EQUALS" 197 | } 198 | ], 199 | "valueValidators": [ 200 | { 201 | "type": "NON_EMPTY" 202 | } 203 | ] 204 | } 205 | ], 206 | "enablingConditions": [ 207 | { 208 | "paramName": "responseStatusCode", 209 | "paramValue": 301, 210 | "type": "EQUALS" 211 | }, 212 | { 213 | "paramName": "responseStatusCode", 214 | "paramValue": 302, 215 | "type": "EQUALS" 216 | } 217 | ] 218 | }, 219 | { 220 | "type": "GROUP", 221 | "name": "clientErrorResponseSettings", 222 | "groupStyle": "NO_ZIPPY", 223 | "subParams": [ 224 | { 225 | "type": "TEXT", 226 | "name": "clientErrorResponseMessage", 227 | "displayName": "Client Error Response Message", 228 | "simpleValueType": true, 229 | "valueValidators": [ 230 | { 231 | "type": "NON_EMPTY" 232 | } 233 | ] 234 | } 235 | ], 236 | "enablingConditions": [ 237 | { 238 | "paramName": "responseStatusCode", 239 | "paramValue": 403, 240 | "type": "EQUALS" 241 | }, 242 | { 243 | "paramName": "responseStatusCode", 244 | "paramValue": 404, 245 | "type": "EQUALS" 246 | } 247 | ] 248 | } 249 | ] 250 | }, 251 | { 252 | "type": "GROUP", 253 | "name": "pathSettings", 254 | "displayName": "Accepted Path Settings", 255 | "groupStyle": "ZIPPY_CLOSED", 256 | "subParams": [ 257 | { 258 | "type": "SIMPLE_TABLE", 259 | "name": "path", 260 | "displayName": "Type additional paths that will be claimed by this client", 261 | "simpleTableColumns": [ 262 | { 263 | "defaultValue": "", 264 | "displayName": "For example: /callback", 265 | "name": "path", 266 | "type": "TEXT", 267 | "isUnique": true, 268 | "valueValidators": [ 269 | { 270 | "type": "NON_EMPTY" 271 | } 272 | ] 273 | } 274 | ], 275 | "newRowButtonText": "Add path", 276 | "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." 277 | } 278 | ] 279 | } 280 | ] 281 | 282 | 283 | ___SANDBOXED_JS_FOR_SERVER___ 284 | 285 | const returnResponse = require('returnResponse'); 286 | const runContainer = require('runContainer'); 287 | const setResponseHeader = require('setResponseHeader'); 288 | const setResponseStatus = require('setResponseStatus'); 289 | const setResponseBody = require('setResponseBody'); 290 | const JSON = require('JSON'); 291 | const fromBase64 = require('fromBase64'); 292 | const getTimestampMillis = require('getTimestampMillis'); 293 | const getCookieValues = require('getCookieValues'); 294 | const getRequestBody = require('getRequestBody'); 295 | const getRequestMethod = require('getRequestMethod'); 296 | const getRequestHeader = require('getRequestHeader'); 297 | const getRequestPath = require('getRequestPath'); 298 | const getRequestQueryParameters = require('getRequestQueryParameters'); 299 | const makeInteger = require('makeInteger'); 300 | const getRemoteAddress = require('getRemoteAddress'); 301 | const setCookie = require('setCookie'); 302 | const setPixelResponse = require('setPixelResponse'); 303 | const generateRandom = require('generateRandom'); 304 | const computeEffectiveTldPlusOne = require('computeEffectiveTldPlusOne'); 305 | const getRequestQueryParameter = require('getRequestQueryParameter'); 306 | const getType = require('getType'); 307 | const Promise = require('Promise'); 308 | const decodeUriComponent = require('decodeUriComponent'); 309 | const createRegex = require('createRegex'); 310 | const makeString = require('makeString'); 311 | 312 | const requestMethod = getRequestMethod(); 313 | const path = getRequestPath(); 314 | let isClientUsed = false; 315 | let isEventModelsWrappedInArray = false; 316 | 317 | if (path === '/data') { 318 | runClient(); 319 | } 320 | 321 | if (data.path && !isClientUsed) { 322 | for (let key in data.path) { 323 | if (!isClientUsed && data.path[key].path === path) { 324 | runClient(); 325 | } 326 | } 327 | } 328 | 329 | function runClient() { 330 | isClientUsed = true; 331 | require('claimRequest')(); 332 | 333 | if (requestMethod === 'OPTIONS') { 334 | setCommonResponseHeaders(200); 335 | returnResponse(); 336 | return; 337 | } 338 | const baseEventModel = getBaseEventModelWithQueryParameters(); 339 | let eventModels = getEventModels(baseEventModel); 340 | const clientId = getClientId(eventModels); 341 | eventModels = eventModels.map((eventModel) => { 342 | eventModel = addRequiredParametersToEventModel(eventModel); 343 | eventModel = addCommonParametersToEventModel(eventModel); 344 | eventModel = addClientIdToEventModel(eventModel, clientId); 345 | return eventModel; 346 | }); 347 | 348 | storeClientId(eventModels[0]); 349 | exposeFPIDCookie(eventModels[0]); 350 | prolongDataTagCookies(eventModels[0]); 351 | const responseStatusCode = makeInteger(data.responseStatusCode); 352 | setCommonResponseHeaders(responseStatusCode); 353 | 354 | Promise.all( 355 | eventModels.map((eventModel) => { 356 | return Promise.create((resolve) => { 357 | runContainer(eventModel, resolve); 358 | }); 359 | }) 360 | ).then(() => { 361 | switch (responseStatusCode) { 362 | case 200: 363 | case 201: 364 | if (requestMethod === 'POST' || data.responseBodyGet) { 365 | prepareResponseBody(eventModels); 366 | } else { 367 | setPixelResponse(); 368 | } 369 | break; 370 | case 301: 371 | case 302: 372 | setRedirectLocation(); 373 | break; 374 | case 403: 375 | case 404: 376 | setClientErrorResponseMessage(); 377 | break; 378 | } 379 | 380 | returnResponse(); 381 | }); 382 | } 383 | 384 | function addCommonParametersToEventModel(eventModel) { 385 | if (!eventModel.ip_override) { 386 | if (eventModel.ip) eventModel.ip_override = eventModel.ip; 387 | else if (eventModel.ipOverride) 388 | eventModel.ip_override = eventModel.ipOverride; 389 | else eventModel.ip_override = getRemoteAddress(); 390 | } 391 | 392 | if (!eventModel.user_agent) { 393 | if (eventModel.userAgent) eventModel.user_agent = eventModel.userAgent; 394 | else if (getRequestHeader('User-Agent')) 395 | eventModel.user_agent = getRequestHeader('User-Agent'); 396 | } 397 | 398 | if (!eventModel.language) { 399 | const acceptLanguageHeader = getRequestHeader('Accept-Language'); 400 | 401 | if (acceptLanguageHeader) { 402 | eventModel.language = acceptLanguageHeader 403 | .split(';')[0] 404 | .substring(0, 2) 405 | .toLowerCase(); 406 | } 407 | } 408 | 409 | if (!eventModel.page_hostname) { 410 | if (eventModel.pageHostname) 411 | eventModel.page_hostname = eventModel.pageHostname; 412 | else if (eventModel.hostname) 413 | eventModel.page_hostname = eventModel.hostname; 414 | } 415 | 416 | if (!eventModel.page_location) { 417 | if (eventModel.pageLocation) 418 | eventModel.page_location = eventModel.pageLocation; 419 | else if (eventModel.url) eventModel.page_location = eventModel.url; 420 | else if (eventModel.href) eventModel.page_location = eventModel.href; 421 | } 422 | 423 | if (!eventModel.page_referrer) { 424 | if (eventModel.pageReferrer) 425 | eventModel.page_referrer = eventModel.pageReferrer; 426 | else if (eventModel.referrer) 427 | eventModel.page_referrer = eventModel.referrer; 428 | else if (eventModel.urlref) eventModel.page_referrer = eventModel.urlref; 429 | } 430 | 431 | if (!eventModel.value && eventModel.e_v) eventModel.value = eventModel.e_v; 432 | 433 | if (getType(eventModel.items) === 'array' && eventModel.items.length) { 434 | const firstItem = eventModel.items[0]; 435 | if (!eventModel.currency && firstItem.currency) 436 | eventModel.currency = firstItem.currency; 437 | if (eventModel.items.length === 1) { 438 | if (!eventModel.item_id && firstItem.item_id) 439 | eventModel.item_id = firstItem.item_id; 440 | if (!eventModel.item_name && firstItem.item_name) 441 | eventModel.item_name = firstItem.item_name; 442 | if (!eventModel.item_brand && firstItem.item_brand) 443 | eventModel.item_brand = firstItem.item_brand; 444 | if (!eventModel.item_quantity && firstItem.quantity) 445 | eventModel.item_quantity = firstItem.quantity; 446 | if (!eventModel.item_category && firstItem.item_category) 447 | eventModel.item_category = firstItem.item_category; 448 | if (!eventModel.item_price && firstItem.price) 449 | eventModel.item_price = firstItem.price; 450 | } 451 | if (!eventModel.value) { 452 | const valueFromItems = eventModel.items.reduce((acc, item) => { 453 | if (!item.price) return acc; 454 | const quantity = item.quantity ? item.quantity : 1; 455 | return acc + quantity * item.price; 456 | }, 0); 457 | if (valueFromItems) eventModel.value = valueFromItems; 458 | } 459 | } 460 | 461 | const ecommerceAction = getEcommerceAction(eventModel); 462 | 463 | if (ecommerceAction) { 464 | if (!eventModel['x-ga-mp1-pa']) eventModel['x-ga-mp1-pa'] = ecommerceAction; 465 | 466 | if ( 467 | ecommerceAction === 'purchase' && 468 | eventModel.ecommerce.purchase.actionField 469 | ) { 470 | if (!eventModel['x-ga-mp1-tr']) 471 | eventModel['x-ga-mp1-tr'] = 472 | eventModel.ecommerce.purchase.actionField.revenue; 473 | if (!eventModel.revenue) 474 | eventModel.revenue = eventModel.ecommerce.purchase.actionField.revenue; 475 | if (!eventModel.affiliation) 476 | eventModel.affiliation = 477 | eventModel.ecommerce.purchase.actionField.affiliation; 478 | if (!eventModel.tax) 479 | eventModel.tax = eventModel.ecommerce.purchase.actionField.tax; 480 | if (!eventModel.shipping) 481 | eventModel.shipping = 482 | eventModel.ecommerce.purchase.actionField.shipping; 483 | if (!eventModel.coupon) 484 | eventModel.coupon = eventModel.ecommerce.purchase.actionField.coupon; 485 | if (!eventModel.transaction_id) 486 | eventModel.transaction_id = 487 | eventModel.ecommerce.purchase.actionField.id; 488 | } 489 | } 490 | 491 | if (!eventModel.page_encoding && eventModel.pageEncoding) 492 | eventModel.page_encoding = eventModel.pageEncoding; 493 | if (!eventModel.page_path && eventModel.pagePath) 494 | eventModel.page_path = eventModel.pagePath; 495 | if (!eventModel.page_title && eventModel.pageTitle) 496 | eventModel.page_title = eventModel.pageTitle; 497 | if (!eventModel.screen_resolution && eventModel.screenResolution) 498 | eventModel.screen_resolution = eventModel.screenResolution; 499 | if (!eventModel.viewport_size && eventModel.viewportSize) 500 | eventModel.viewport_size = eventModel.viewportSize; 501 | if (!eventModel.user_id && eventModel.userId) 502 | eventModel.user_id = eventModel.userId; 503 | 504 | if (!eventModel.user_data) { 505 | let userData = {}; 506 | let userAddressData = {}; 507 | 508 | if (!userData.email_address) { 509 | if (eventModel.userEmail) userData.email_address = eventModel.userEmail; 510 | else if (eventModel.email_address) 511 | userData.email_address = eventModel.email_address; 512 | else if (eventModel.email) userData.email_address = eventModel.email; 513 | else if (eventModel.mail) userData.email_address = eventModel.mail; 514 | } 515 | 516 | if (!userData.phone_number) { 517 | if (eventModel.userPhoneNumber) 518 | userData.phone_number = eventModel.userPhoneNumber; 519 | else if (eventModel.phone_number) 520 | userData.phone_number = eventModel.phone_number; 521 | else if (eventModel.phoneNumber) 522 | userData.phone_number = eventModel.phoneNumber; 523 | else if (eventModel.phone) userData.phone_number = eventModel.phone; 524 | } 525 | 526 | if (!userAddressData.street && eventModel.street) 527 | userAddressData.street = eventModel.street; 528 | if (!userAddressData.city && eventModel.city) 529 | userAddressData.city = eventModel.city; 530 | if (!userAddressData.region && eventModel.region) 531 | userAddressData.region = eventModel.region; 532 | if (!userAddressData.country && eventModel.country) 533 | userAddressData.country = eventModel.country; 534 | 535 | if (!userAddressData.first_name) { 536 | if (eventModel.userFirstName) 537 | userAddressData.first_name = eventModel.userFirstName; 538 | else if (eventModel.first_name) 539 | userAddressData.first_name = eventModel.first_name; 540 | else if (eventModel.firstName) 541 | userAddressData.first_name = eventModel.firstName; 542 | else if (eventModel.name) userAddressData.first_name = eventModel.name; 543 | } 544 | 545 | if (!userAddressData.last_name) { 546 | if (eventModel.userLastName) 547 | userAddressData.last_name = eventModel.userLastName; 548 | else if (eventModel.last_name) 549 | userAddressData.last_name = eventModel.last_name; 550 | else if (eventModel.lastName) 551 | userAddressData.last_name = eventModel.lastName; 552 | else if (eventModel.surname) 553 | userAddressData.last_name = eventModel.surname; 554 | else if (eventModel.family_name) 555 | userAddressData.last_name = eventModel.family_name; 556 | else if (eventModel.familyName) 557 | userAddressData.last_name = eventModel.familyName; 558 | } 559 | 560 | if (!userAddressData.region) { 561 | if (eventModel.region) userAddressData.region = eventModel.region; 562 | else if (eventModel.state) userAddressData.region = eventModel.state; 563 | } 564 | 565 | if (!userAddressData.postal_code) { 566 | if (eventModel.postal_code) 567 | userAddressData.postal_code = eventModel.postal_code; 568 | else if (eventModel.postalCode) 569 | userAddressData.postal_code = eventModel.postalCode; 570 | else if (eventModel.zip) userAddressData.postal_code = eventModel.zip; 571 | } 572 | 573 | if (getObjectLength(userAddressData) !== 0) { 574 | userData.address = userAddressData; 575 | } 576 | 577 | if (!eventModel.user_data && getObjectLength(userData) !== 0) { 578 | eventModel.user_data = userData; 579 | } 580 | } 581 | 582 | return eventModel; 583 | } 584 | 585 | function getBaseEventModelWithQueryParameters() { 586 | const requestQueryParameters = getRequestQueryParameters(); 587 | const eventModel = {}; 588 | 589 | if (requestQueryParameters) { 590 | for (let queryParameterKey in requestQueryParameters) { 591 | if ( 592 | (queryParameterKey === 'dtcd' || queryParameterKey === 'dtdc') && 593 | requestMethod === 'GET' 594 | ) { 595 | let dt = 596 | queryParameterKey === 'dtcd' 597 | ? JSON.parse(requestQueryParameters[queryParameterKey]) 598 | : JSON.parse(fromBase64(requestQueryParameters[queryParameterKey])); 599 | 600 | for (let dtKey in dt) { 601 | eventModel[dtKey] = dt[dtKey]; 602 | } 603 | } else { 604 | eventModel[queryParameterKey] = 605 | requestQueryParameters[queryParameterKey]; 606 | } 607 | } 608 | } 609 | 610 | return eventModel; 611 | } 612 | 613 | function addClientIdToEventModel(eventModel, clientId) { 614 | eventModel.client_id = clientId; 615 | return eventModel; 616 | } 617 | 618 | function prolongDataTagCookies(eventModel) { 619 | if (data.prolongCookies) { 620 | let stapeData = getCookieValues('stape'); 621 | 622 | if (stapeData.length) { 623 | setCookie('stape', stapeData[0], { 624 | domain: 'auto', 625 | path: '/', 626 | samesite: getCookieType(eventModel), 627 | secure: true, 628 | 'max-age': 63072000, // 2 years 629 | httpOnly: false 630 | }); 631 | } 632 | } 633 | } 634 | 635 | function addRequiredParametersToEventModel(eventModel) { 636 | if (!eventModel.event_name) { 637 | let eventName = 'Data'; 638 | 639 | if (eventModel.eventName) eventName = eventModel.eventName; 640 | else if (eventModel.event) eventName = eventModel.event; 641 | else if (eventModel.e_n) eventName = eventModel.e_n; 642 | 643 | eventModel.event_name = eventName; 644 | } 645 | 646 | return eventModel; 647 | } 648 | 649 | function exposeFPIDCookie(eventModel) { 650 | if (data.exposeFPIDCookie) { 651 | let fpid = getCookieValues('FPID'); 652 | 653 | if (fpid.length) { 654 | setCookie('FPIDP', fpid[0], { 655 | domain: 'auto', 656 | path: '/', 657 | samesite: getCookieType(eventModel), 658 | secure: true, 659 | 'max-age': 63072000, // 2 years 660 | httpOnly: false 661 | }); 662 | } 663 | } 664 | } 665 | 666 | function storeClientId(eventModel) { 667 | if (data.generateClientId) { 668 | setCookie('_dcid', eventModel.client_id, { 669 | domain: 'auto', 670 | path: '/', 671 | samesite: getCookieType(eventModel), 672 | secure: true, 673 | 'max-age': 63072000, // 2 years 674 | httpOnly: data.httpOnlyCookie || false 675 | }); 676 | } 677 | } 678 | 679 | function getObjectLength(object) { 680 | let length = 0; 681 | 682 | for (let key in object) { 683 | if (object.hasOwnProperty(key)) { 684 | ++length; 685 | } 686 | } 687 | return length; 688 | } 689 | 690 | function setCommonResponseHeaders(statusCode) { 691 | setResponseHeader('Access-Control-Max-Age', '600'); 692 | setResponseHeader('Access-Control-Allow-Origin', getRequestHeader('origin')); 693 | setResponseHeader( 694 | 'Access-Control-Allow-Methods', 695 | 'GET,POST,PUT,DELETE,OPTIONS' 696 | ); 697 | setResponseHeader( 698 | 'Access-Control-Allow-Headers', 699 | 'content-type,set-cookie,x-robots-tag,x-gtm-server-preview,x-stape-preview' 700 | ); 701 | setResponseHeader('Access-Control-Allow-Credentials', 'true'); 702 | setResponseStatus(statusCode); 703 | } 704 | 705 | function getCookieType(eventModel) { 706 | if (!eventModel.page_location) { 707 | return 'Lax'; 708 | } 709 | 710 | const host = getRequestHeader('host'); 711 | const effectiveTldPlusOne = computeEffectiveTldPlusOne( 712 | eventModel.page_location 713 | ); 714 | 715 | if (!host || !effectiveTldPlusOne) { 716 | return 'Lax'; 717 | } 718 | 719 | if (host && host.indexOf(effectiveTldPlusOne) !== -1) { 720 | return 'Lax'; 721 | } 722 | 723 | return 'None'; 724 | } 725 | 726 | function prepareResponseBody(eventModels) { 727 | if (data.responseBody === 'empty') { 728 | return; 729 | } 730 | 731 | const responseModel = isEventModelsWrappedInArray 732 | ? eventModels[0] 733 | : eventModels; 734 | 735 | setResponseHeader('Content-Type', 'application/json'); 736 | 737 | if (data.responseBody === 'eventData') { 738 | setResponseBody(JSON.stringify(responseModel)); 739 | 740 | return; 741 | } 742 | 743 | if (isEventModelsWrappedInArray) { 744 | setResponseBody( 745 | JSON.stringify({ 746 | timestamp: responseModel.timestamp, 747 | unique_event_id: responseModel.unique_event_id 748 | }) 749 | ); 750 | return; 751 | } 752 | 753 | setResponseBody( 754 | JSON.stringify( 755 | eventModels.map((eventModel) => { 756 | return { 757 | timestamp: eventModel.timestamp, 758 | unique_event_id: eventModel.unique_event_id 759 | }; 760 | }) 761 | ) 762 | ); 763 | } 764 | 765 | function getEcommerceAction(eventModel) { 766 | if (eventModel.ecommerce) { 767 | const actions = [ 768 | 'detail', 769 | 'click', 770 | 'add', 771 | 'remove', 772 | 'checkout', 773 | 'checkout_option', 774 | 'purchase', 775 | 'refund' 776 | ]; 777 | 778 | for (let index = 0; index < actions.length; ++index) { 779 | const action = actions[index]; 780 | 781 | if (eventModel.ecommerce[action]) { 782 | return action; 783 | } 784 | } 785 | } 786 | 787 | return null; 788 | } 789 | 790 | function setRedirectLocation() { 791 | let location = data.redirectTo; 792 | if (data.lookupForRedirectToParam && data.redirectToQueryParamName) { 793 | const param = getRequestQueryParameter(data.redirectToQueryParamName); 794 | if (param && param.startsWith('http')) { 795 | location = param; 796 | } 797 | } 798 | setResponseHeader('location', location); 799 | } 800 | 801 | function setClientErrorResponseMessage() { 802 | if (data.clientErrorResponseMessage) { 803 | setResponseBody(data.clientErrorResponseMessage); 804 | } 805 | } 806 | 807 | function getEventModels(baseEventModel) { 808 | const body = getRequestBody(); 809 | 810 | if (body) { 811 | const contentType = getRequestHeader('content-type'); 812 | const isFormUrlEncoded = 813 | !!contentType && 814 | contentType.indexOf('application/x-www-form-urlencoded') !== -1; 815 | let bodyJson = isFormUrlEncoded ? parseUrlEncoded(body) : JSON.parse(body); 816 | if (bodyJson) { 817 | const bodyType = getType(bodyJson); 818 | const shouldUseOriginalBody = 819 | data.acceptMultipleEvents && bodyType === 'array'; 820 | if (!shouldUseOriginalBody) { 821 | bodyJson = [bodyJson]; 822 | isEventModelsWrappedInArray = true; 823 | } 824 | 825 | return bodyJson.map((bodyItem) => { 826 | const eventModel = assign( 827 | { 828 | timestamp: makeInteger(getTimestampMillis() / 1000), 829 | unique_event_id: 830 | getTimestampMillis() + '_' + generateRandom(100000000, 999999999) 831 | }, 832 | baseEventModel 833 | ); 834 | for (let bodyItemKey in bodyItem) { 835 | eventModel[bodyItemKey] = bodyItem[bodyItemKey]; 836 | } 837 | return eventModel; 838 | }); 839 | } 840 | } 841 | 842 | return [ 843 | assign( 844 | { 845 | timestamp: makeInteger(getTimestampMillis() / 1000), 846 | unique_event_id: 847 | getTimestampMillis() + '_' + generateRandom(100000000, 999999999) 848 | }, 849 | baseEventModel 850 | ) 851 | ]; 852 | } 853 | 854 | function getClientId(eventModels) { 855 | for (let i = 0; i < eventModels.length; i++) { 856 | const eventModel = eventModels[i]; 857 | const clientId = 858 | eventModel.client_id || eventModel.data_client_id || eventModel._dcid; 859 | if (clientId) return clientId; 860 | } 861 | 862 | const dcid = getCookieValues('_dcid'); 863 | if (dcid && dcid[0]) return dcid[0]; 864 | 865 | if (data.generateClientId) { 866 | return ( 867 | 'dcid.1.' + 868 | getTimestampMillis() + 869 | '.' + 870 | generateRandom(100000000, 999999999) 871 | ); 872 | } 873 | return ''; 874 | } 875 | 876 | function assign() { 877 | const target = arguments[0]; 878 | for (let i = 1; i < arguments.length; i++) { 879 | for (let key in arguments[i]) { 880 | target[key] = arguments[i][key]; 881 | } 882 | } 883 | return target; 884 | } 885 | 886 | function parseUrlEncoded(data) { 887 | const pairs = data.split('&'); 888 | const parsedData = {}; 889 | const regex = createRegex('\\+', 'g'); 890 | for (const pair of pairs) { 891 | const pairValue = pair.split('='); 892 | const key = pairValue[0]; 893 | const value = pairValue[1]; 894 | const keys = key 895 | .split('.') 896 | .map((k) => decodeUriComponent(k.replace(regex, ' '))); 897 | 898 | let currentObject = parsedData; 899 | 900 | for (let i = 0; i < keys.length - 1; i++) { 901 | const currentKey = keys[i]; 902 | 903 | if (!currentObject[currentKey]) { 904 | const nextKey = keys[i + 1]; 905 | const nextKeyIsNumber = makeString(makeInteger(nextKey)) === nextKey; 906 | currentObject[currentKey] = nextKeyIsNumber ? [] : {}; 907 | } 908 | 909 | currentObject = currentObject[currentKey]; 910 | } 911 | 912 | const lastKey = keys[keys.length - 1]; 913 | const decodedValue = decodeUriComponent(value.replace(regex, ' ')); 914 | const parsedValue = JSON.parse(decodedValue) || decodedValue; 915 | 916 | if (getType(currentObject) === 'array') { 917 | currentObject.push(parsedValue); 918 | } else { 919 | currentObject[lastKey] = parsedValue; 920 | } 921 | } 922 | 923 | return parsedData; 924 | } 925 | 926 | 927 | ___SERVER_PERMISSIONS___ 928 | 929 | [ 930 | { 931 | "instance": { 932 | "key": { 933 | "publicId": "return_response", 934 | "versionId": "1" 935 | }, 936 | "param": [] 937 | }, 938 | "isRequired": true 939 | }, 940 | { 941 | "instance": { 942 | "key": { 943 | "publicId": "access_response", 944 | "versionId": "1" 945 | }, 946 | "param": [ 947 | { 948 | "key": "writeResponseAccess", 949 | "value": { 950 | "type": 1, 951 | "string": "any" 952 | } 953 | }, 954 | { 955 | "key": "writeHeaderAccess", 956 | "value": { 957 | "type": 1, 958 | "string": "specific" 959 | } 960 | } 961 | ] 962 | }, 963 | "clientAnnotations": { 964 | "isEditedByUser": true 965 | }, 966 | "isRequired": true 967 | }, 968 | { 969 | "instance": { 970 | "key": { 971 | "publicId": "run_container", 972 | "versionId": "1" 973 | }, 974 | "param": [] 975 | }, 976 | "isRequired": true 977 | }, 978 | { 979 | "instance": { 980 | "key": { 981 | "publicId": "get_cookies", 982 | "versionId": "1" 983 | }, 984 | "param": [ 985 | { 986 | "key": "cookieAccess", 987 | "value": { 988 | "type": 1, 989 | "string": "specific" 990 | } 991 | }, 992 | { 993 | "key": "cookieNames", 994 | "value": { 995 | "type": 2, 996 | "listItem": [ 997 | { 998 | "type": 1, 999 | "string": "stape" 1000 | }, 1001 | { 1002 | "type": 1, 1003 | "string": "_dcid" 1004 | }, 1005 | { 1006 | "type": 1, 1007 | "string": "FPIDP" 1008 | }, 1009 | { 1010 | "type": 1, 1011 | "string": "FPID" 1012 | } 1013 | ] 1014 | } 1015 | } 1016 | ] 1017 | }, 1018 | "clientAnnotations": { 1019 | "isEditedByUser": true 1020 | }, 1021 | "isRequired": true 1022 | }, 1023 | { 1024 | "instance": { 1025 | "key": { 1026 | "publicId": "read_request", 1027 | "versionId": "1" 1028 | }, 1029 | "param": [ 1030 | { 1031 | "key": "requestAccess", 1032 | "value": { 1033 | "type": 1, 1034 | "string": "any" 1035 | } 1036 | }, 1037 | { 1038 | "key": "headerAccess", 1039 | "value": { 1040 | "type": 1, 1041 | "string": "any" 1042 | } 1043 | }, 1044 | { 1045 | "key": "queryParameterAccess", 1046 | "value": { 1047 | "type": 1, 1048 | "string": "any" 1049 | } 1050 | } 1051 | ] 1052 | }, 1053 | "clientAnnotations": { 1054 | "isEditedByUser": true 1055 | }, 1056 | "isRequired": true 1057 | }, 1058 | { 1059 | "instance": { 1060 | "key": { 1061 | "publicId": "set_cookies", 1062 | "versionId": "1" 1063 | }, 1064 | "param": [ 1065 | { 1066 | "key": "allowedCookies", 1067 | "value": { 1068 | "type": 2, 1069 | "listItem": [ 1070 | { 1071 | "type": 3, 1072 | "mapKey": [ 1073 | { 1074 | "type": 1, 1075 | "string": "name" 1076 | }, 1077 | { 1078 | "type": 1, 1079 | "string": "domain" 1080 | }, 1081 | { 1082 | "type": 1, 1083 | "string": "path" 1084 | }, 1085 | { 1086 | "type": 1, 1087 | "string": "secure" 1088 | }, 1089 | { 1090 | "type": 1, 1091 | "string": "session" 1092 | } 1093 | ], 1094 | "mapValue": [ 1095 | { 1096 | "type": 1, 1097 | "string": "stape" 1098 | }, 1099 | { 1100 | "type": 1, 1101 | "string": "*" 1102 | }, 1103 | { 1104 | "type": 1, 1105 | "string": "*" 1106 | }, 1107 | { 1108 | "type": 1, 1109 | "string": "any" 1110 | }, 1111 | { 1112 | "type": 1, 1113 | "string": "any" 1114 | } 1115 | ] 1116 | }, 1117 | { 1118 | "type": 3, 1119 | "mapKey": [ 1120 | { 1121 | "type": 1, 1122 | "string": "name" 1123 | }, 1124 | { 1125 | "type": 1, 1126 | "string": "domain" 1127 | }, 1128 | { 1129 | "type": 1, 1130 | "string": "path" 1131 | }, 1132 | { 1133 | "type": 1, 1134 | "string": "secure" 1135 | }, 1136 | { 1137 | "type": 1, 1138 | "string": "session" 1139 | } 1140 | ], 1141 | "mapValue": [ 1142 | { 1143 | "type": 1, 1144 | "string": "_dcid" 1145 | }, 1146 | { 1147 | "type": 1, 1148 | "string": "*" 1149 | }, 1150 | { 1151 | "type": 1, 1152 | "string": "*" 1153 | }, 1154 | { 1155 | "type": 1, 1156 | "string": "any" 1157 | }, 1158 | { 1159 | "type": 1, 1160 | "string": "any" 1161 | } 1162 | ] 1163 | }, 1164 | { 1165 | "type": 3, 1166 | "mapKey": [ 1167 | { 1168 | "type": 1, 1169 | "string": "name" 1170 | }, 1171 | { 1172 | "type": 1, 1173 | "string": "domain" 1174 | }, 1175 | { 1176 | "type": 1, 1177 | "string": "path" 1178 | }, 1179 | { 1180 | "type": 1, 1181 | "string": "secure" 1182 | }, 1183 | { 1184 | "type": 1, 1185 | "string": "session" 1186 | } 1187 | ], 1188 | "mapValue": [ 1189 | { 1190 | "type": 1, 1191 | "string": "FPIDP" 1192 | }, 1193 | { 1194 | "type": 1, 1195 | "string": "*" 1196 | }, 1197 | { 1198 | "type": 1, 1199 | "string": "*" 1200 | }, 1201 | { 1202 | "type": 1, 1203 | "string": "any" 1204 | }, 1205 | { 1206 | "type": 1, 1207 | "string": "any" 1208 | } 1209 | ] 1210 | } 1211 | ] 1212 | } 1213 | } 1214 | ] 1215 | }, 1216 | "clientAnnotations": { 1217 | "isEditedByUser": true 1218 | }, 1219 | "isRequired": true 1220 | } 1221 | ] 1222 | 1223 | 1224 | ___TESTS___ 1225 | 1226 | scenarios: 1227 | - name: Quick Test 1228 | code: runCode(); 1229 | setup: '' 1230 | 1231 | 1232 | ___NOTES___ 1233 | 1234 | Created on 21/03/2021, 11:24:30 1235 | 1236 | --------------------------------------------------------------------------------