├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── metadata.yaml └── template.tpl /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | ## Released 4 | 5 | 0.0.9 (Sept 5, 2023) 6 | * Add secure, HTTP only, 1st party `_gtmeec` cookie to enhance event data. This new cookie can be enabled using 'Enable Event Enhancement' option from the tag. 7 | 8 | Toggling this feature “on” will help maximize the performance benefits of the events you currently share with Meta. Using this feature may improve your server events’ Event Match Quality (EMQ) by enabling first party http only secure cookie `_gtmeec` to cache hashed PII that you share using the Meta Pixel through Advanced Matching. 9 | 10 | * Add [Data Processing Options](https://developers.facebook.com/docs/marketing-apis/data-processing-options/) 11 | * Fix null value set parameters. 12 | * Correct phone normalisation was also fixed in these changes. 13 | * Invalid template correction and test fixes. 14 | 15 | 0.0.8 (May 22, 2023) 16 | * Update Conversions API Graph version to v16.0 17 | * Fix syntax error in GTM template for extending meta cookies (fbp/fbc). 18 | * Fix contents array parsing. 19 | * Fix contents array received as [object Object]. 20 | * Enable custom parameters. 21 | * Added fb_login_id support. 22 | * Added Meta Logo. 23 | 24 | 0.0.7 (Feb 27, 2022) 25 | * Update Conversions API Graph version to v14.0 26 | 27 | 0.0.6 (Jan 24, 2022) 28 | * Upgrading Facebook references to Meta. 29 | 30 | 0.0.5 (Aug 5, 2021) 31 | * Map gtm.dom event_names to PageView 32 | 33 | 0.0.4 (Jun 15, 2021) 34 | * Update Conversions API Graph version to v11.0 35 | 36 | 0.0.3 (Feb 24, 2021) 37 | * Update Conversions API Graph version to v10.0 38 | * Published in [Google Tag Manager Template Gallery](https://tagmanager.google.com/gallery/#/owners/facebookincubator/templates/ConversionsAPI-Tag-for-GoogleTagManager) 39 | 40 | 0.0.2 (Dec 22, 2020) 41 | * Update Conversions API Graph version to v9.0 42 | 43 | 0.0.1 (Oct 2, 2020) 44 | * Tag for deploying on Google Tag Manager's server-side to send Conversions API events. 45 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Facebook has adopted a Code of Conduct that we expect project participants to adhere to. 4 | Please read the [full text](https://code.fb.com/codeofconduct/) 5 | so that you can understand what actions will and will not be tolerated. 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ConversionsAPI-Tag-for-GoogleTagManager 2 | We want to make contributing to this project as easy and transparent as possible. 3 | 4 | ## Our Development Process 5 | Clone this repo with the following command: 6 | Run `$ git clone https://github.com/facebookincubator/ConversionsAPI-Tag-for-GoogleTagManager.git`. 7 | 8 | ## Initiate the development environment: 9 | 1. Follow Google Tag Manager's guidance for [updating templates](https://developers.google.com/tag-manager/templates/gallery#build_your_template). 10 | 2. Make your changes to the `template.tpl` file and verify by deploying your template to your Google Tag Manager's server container. 11 | 3. Include any test plan/screenshot that is relevant to your pull request. 12 | 13 | ## Pull Requests 14 | We actively welcome your pull requests. 15 | 16 | 1. Fork the repo and create your branch from `master`. 17 | 2. If you've added code that should be tested, add tests. 18 | 3. If you've changed APIs, update the documentation. 19 | 4. Make sure your code lints. 20 | 5. If you haven't already, complete the Contributor License Agreement ("CLA"). 21 | 22 | ## Contributor License Agreement ("CLA") 23 | In order to accept your pull request, we need you to submit a CLA. You only need 24 | to do this once to work on any of Facebook's open source projects. 25 | 26 | Complete your CLA here: . 27 | 28 | ## Issues 29 | We use GitHub issues to track public bugs. Please ensure your description is 30 | clear and has sufficient instructions to be able to reproduce the issue. 31 | 32 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe 33 | disclosure of security bugs. In those cases, please go through the process 34 | outlined on that page and do not file a public issue. 35 | 36 | ## License 37 | By contributing to ConversionsAPI-Tag-for-GoogleTagManager, you agree that your contributions 38 | will be licensed under the LICENSE file in the root directory of 39 | this source tree. 40 | -------------------------------------------------------------------------------- /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 [2020] Facebook, Inc. 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 | # Conversions API Tag for Google Tag Manager(server-side) 2 | 3 | The Conversions API allows advertisers to send web events from their servers directly to Conversions API. Server events are linked to a pixel and are processed like browser pixel events. This means that server events are used in measurement, reporting, and optimization in the same way as browser pixel events. [Learn More](https://developers.facebook.com/docs/marketing-api/conversions-api). 4 | 5 | Google Tag Manager(GTM) have released support for their [Server-side](https://developers.google.com/tag-manager/serverside/) support for sharing customer actions with analytics partners directly from Advertisers' servers. 6 | 7 | This template is a tag that reads the standard event schema that’s sent from the client running on a tagging server. It then converts events to the appropriate schema and sends them through Conversions API. The Tag supports the below clients, 8 | 9 | * [GA4 Client](https://developers.google.com/tag-manager/serverside/send-data#server-side_client_configuration) 10 | * [Conversions API Client](https://github.com/facebookincubator/ConversionsAPI-Client-for-GoogleTagManager) (In Beta.This feature may not be available to you. Contact your Conversions API representative for more details.) 11 | 12 | # Installation 13 | 14 | Please follow the instructions [here](https://www.facebook.com/business/help/702509907046774). 15 | 16 | # Reporting Bugs/Feedback 17 | Please raise any issue on GitHub 18 | 19 | # License 20 | Conversions API for Google Tag Manager is licensed under LICENSE file in the root directory of this source tree. 21 | -------------------------------------------------------------------------------- /metadata.yaml: -------------------------------------------------------------------------------- 1 | homepage: "https://developers.facebook.com/docs/marketing-api/conversions-api" 2 | documentation: "https://developers.facebook.com/docs/marketing-api/conversions-api/guides/gtm-server-side" 3 | versions: 4 | # Latest version 5 | - sha: a495e1bb54b3b1fe6e7a9f10fc64aa12e8731428 6 | changeNotes: Minor bug fix. 7 | # Older versions 8 | - sha: 54def96730fc5f25bcf469c0bfe14b90e302c855 9 | changeNotes: Invalid template correction to fetch new features, updates and test fixes. See CHANGELOG.md 10 | - sha: 57ed834ad33607799a72eefc6dfdf5c25b395f7a 11 | changeNotes: Version update with new features and bug fixes. See CHANGELOG.md 12 | - sha: f261eb2d0858b455618569cdfa8c752f2d543568 13 | changeNotes: Fix test setup 14 | - sha: 60e6504b1bff94f57a22abd9c7bc7c4043de44d7 15 | changeNotes: API version bump to v16.0 for GTM Plugin 16 | 1. Fixing syntax error in GTM template for extending meta cookies (fbp/fbc). 17 | 2. Fix contents array parsing. 18 | 3. Fix contents array received as [object Object]. 19 | 4. Enable custom parameters. 20 | 5. Added fb_login_id support. 21 | 6. Added Meta Logo. 22 | - sha: b0c8f9608085738725806125aa849b8b959ed70c 23 | changeNotes: API version bump for GTM Plugin 24 | - sha: e6da8b1053e39c03fec77c455e6aae839344da43 25 | changeNotes: Updating Facebook references to Meta 26 | - sha: 1c8df2efbf77f0913863399ada05ae5c6895636d 27 | changeNotes: Map gtm.dom event_names to PageView 28 | - sha: 6ea79a611f497daabc37cce5d0ed1447685cc930 29 | changeNotes: Updated to latest Graph API v11.0 30 | - sha: f344a4c9e09aa135ecdb5aa7c945e698efce12c4 31 | changeNotes: Bug fixes for issues reported on GitHub. 32 | # Initial "Public" Version for Conversions API Tag 33 | - sha: 47daf13d1de21fbf2de68d4a2926581ee39d79a3 34 | changeNotes: Added support for GA4 Web Tag and Client to post event to Conversions API. 35 | - sha: 329d13128703af9bad62090dc7ff6b19e57ed4cc 36 | changeNotes: Adding support for converting common event model with Meta Pixel specific parameters to Conversions API. 37 | -------------------------------------------------------------------------------- /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": "TAG", 13 | "id": "cvt_temp_public_id", 14 | "version": 1, 15 | "securityGroups": [], 16 | "displayName": "Conversions API Tag", 17 | "brand": { 18 | "id": "brand_dummy", 19 | "displayName": "", 20 | "thumbnail": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/7QCEUGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAGgcAigAYkZCTUQwYTAwMGE2ZTAxMDAwMGU3MDEwMDAwNmIwMjAwMDBjNTAyMDAwMDM0MDMwMDAwODMwNDAwMDAyOTA1MDAwMDcyMDUwMDAwY2YwNTAwMDAzMjA2MDAwMDEwMDcwMDAwAP/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFhQYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/CABEIAGAAYAMBIgACEQEDEQH/xAAaAAEBAQEBAQEAAAAAAAAAAAAABwUGBAgD/8QAGQEBAAMBAQAAAAAAAAAAAAAAAAECBAMF/9oADAMBAAIQAxAAAAH6pAAAAAAAAAAAAMq1dVIdL0sFMcNgeftrCIdPS1IR/fKCwd6Jcx07rznf5UlsyzTw1lk0yHjvo9z6Snw2RCfUEiQSAAAAAAAAAAB//8QAHRAAAwEAAgMBAAAAAAAAAAAAAwQFBgECEBRQFv/aAAgBAQABBQL7tR/rMnkDYWmocuXUGrHdnI4zRGPPzOidoaMG5WL3HrXP1LGyAN2HZDeneNKiSjGHw1fYKuznqvE5nrjecaOzJfgnd0S0ohlGvYkbGSZ7LlwibCOc+7//xAAhEQABAwMEAwAAAAAAAAAAAAABAAIDERIxFCEwQRAiUf/aAAgBAwEBPwHkc4NFxWr3tsNc9YTXteA5uCqqviSMSsMZ7Wl2rd7fVHGI2Bg6VFTm/8QAJhEAAQQBAQcFAAAAAAAAAAAAAgABAxExEgQQExQhMEEiYZGh8P/aAAgBAgEBPwHuCLmTCOXT7DQ6+I1XV9c/H2jjICcSyypVuikeGQZByy5zrWn0Yr9591LK8pvI/lWr73//xAAwEAABAwIDBQYGAwAAAAAAAAADAQIEABEFEjEUQUJRgRMhIiRQUhAVIzIzwWFykf/aAAgBAQAGPwL108pyXQbb25183JiSodE7V0VyfSt7aXEQz3iPdcgGfjbbhdzqVPEvYmSO93dwuRFooMVJ5qONJCldxhcl0d+qmFmEUWHvibUELtGDzWRy9Ev1oL3Q5YYJnowc0g7Dcu7+USpUVYE18Zo25QtE3M1c1s+v20cAIUyayOuU5o47sGv76UyZHa9onqqJn17lt8ZARJci2VG87Leo6HiEhwgWe5hNSP3dKNJixnyoUrxECLVj+aVPCol2k4zOQKa3deyVg+0PLEMGM0Jkbq9lkuN3+VibGDUMY2FbOwtvAjr6UDDpmH468yZWEZtHl+7ei6W30SbsMmXGkxmBR8ZubK5Hb6nwVwqVN7SS8wDgRMj83uXhoIpIXAMjyKrHf3X17//EACQQAQABBAEEAQUAAAAAAAAAAAERACExUUFQYYGhEHGRsfDx/9oACAEBAAE/IeuzurO7g8sVHzEES67j396aCrH7ISU8veklbezyJ0lGBIMT1HuvpTycQpwKTKlAYJPKMcpDhSjGl7qJN5DF+MV3zg3wZlHIGgyBxC4lZdfMNadmkPVP2OxRYRrmrWrgnXNP7grLrlIDycVjItOUdpP5UQeVvkY8SLapHKm2RF62IgeKN6Dw2sZIIZmhDjNAcw7ccr/SDBPCSh469//aAAwDAQACAAMAAAAQ888888888888+Lkb31tf1Az3888888888888/8QAHhEBAQACAgIDAAAAAAAAAAAAAREAITFhMIFBUXH/2gAIAQMBAT8Q8iNYBX8MtJpIOXbnn4nN9uOxQEenAuTg3ZnFaEfZMCUW943RJJJNTted5t0AG+snPtcCE8v/xAAcEQEBAAIDAQEAAAAAAAAAAAABEQAhMVFhMEH/2gAIAQIBAT8Q+hxVADtdGCijrwHXx+/iewQWiInppxZlZJnPEiXsblkTpysi223be10HBMCyKXXuDM6TFrfr/8QAIRABAQACAgEEAwAAAAAAAAAAAREAITFBURAgUGFxkdH/2gAIAQEAAT8Q+Svqbyll372/DAzrv1QX7yDhDTJByLAPKpbs7y1zdIaoxVOKEmEC4MIxyOWl6lxxL5AHaAMqQIi7c17Vk04AUquq8ZfdpnsL1EMOrOlKZaqmoxbVpMZBF+hZXUg7QEZcUYi13EAi0bsSx163XZCFbS6qIPuYLoSZnT2Xk8/mRqRwwv8Abzy2bIjxPeAhzsJ5Zh4aZj9U4/SXywU8WSBAn2EGNZmv83j+MpAcXEuGOppskzpnmMABrnKCYklGtgwxJ93Ftp5CI9iPunpMhkyGBPkv/9k\u003d" 21 | }, 22 | "description": "A server-side tag template that prepares information from your tagging server to be sent through Conversions API.", 23 | "containerContexts": [ 24 | "SERVER" 25 | ] 26 | } 27 | 28 | 29 | ___TEMPLATE_PARAMETERS___ 30 | 31 | [ 32 | { 33 | "type": "TEXT", 34 | "name": "pixelId", 35 | "displayName": "Pixel ID", 36 | "simpleValueType": true, 37 | "valueValidators": [ 38 | { 39 | "type": "NON_EMPTY" 40 | } 41 | ] 42 | }, 43 | { 44 | "type": "TEXT", 45 | "name": "apiAccessToken", 46 | "displayName": "API Access Token", 47 | "simpleValueType": true, 48 | "valueValidators": [ 49 | { 50 | "type": "NON_EMPTY" 51 | } 52 | ], 53 | "help": "To use the Conversions API, you need an access token. See \u003ca href\u003d\"https://developers.facebook.com/docs/marketing-api/conversions-api/get-started#access-token\"\u003ehere\u003c/a\u003e for generating an access token." 54 | }, 55 | { 56 | "type": "TEXT", 57 | "name": "testEventCode", 58 | "displayName": "Test Event Code", 59 | "simpleValueType": true, 60 | "help": "Code used to verify that your server events are received correctly by Conversions API. Use this code to test your server events in the Test Events feature in Events Manager. See \u003ca href\u003d\"https://developers.facebook.com/docs/marketing-api/conversions-api/using-the-api#testEvents\"\u003e Test Events Tool\u003c/a\u003e for an example." 61 | }, 62 | { 63 | "type": "SELECT", 64 | "name": "actionSource", 65 | "displayName": "Action Source", 66 | "macrosInSelect": false, 67 | "selectItems": [ 68 | { 69 | "value": "website", 70 | "displayValue": "Website" 71 | }, 72 | { 73 | "value": "email", 74 | "displayValue": "Email" 75 | }, 76 | { 77 | "value": "app", 78 | "displayValue": "App" 79 | }, 80 | { 81 | "value": "phone_call", 82 | "displayValue": "Phone Call" 83 | }, 84 | { 85 | "value": "chat", 86 | "displayValue": "Chat" 87 | }, 88 | { 89 | "value": "physical_store", 90 | "displayValue": "Physical Store" 91 | }, 92 | { 93 | "value": "system_generated", 94 | "displayValue": "System Generated" 95 | }, 96 | { 97 | "value": "other", 98 | "displayValue": "Other" 99 | } 100 | ], 101 | "simpleValueType": true, 102 | "help": "This field allows you to specify where your conversions occurred. Knowing where your events took place helps ensure your ads go to the right people. See \u003ca href\u003d\"https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/server-event#action-source\"\u003ehere\u003c/a\u003e for more information." 103 | }, 104 | { 105 | "type": "CHECKBOX", 106 | "name": "extendCookies", 107 | "checkboxText": "Extend Meta Pixel cookies (fbp/fbc)", 108 | "simpleValueType": true 109 | }, 110 | { 111 | "type": "CHECKBOX", 112 | "name": "enableEventEnhancement", 113 | "checkboxText": "Enable Event Enhancement", 114 | "simpleValueType": true, 115 | "help": "Enable Use of HTTP Only Secure Cookie (gtmeec) to Enhance Event Data" 116 | } 117 | ] 118 | 119 | 120 | ___SANDBOXED_JS_FOR_SERVER___ 121 | 122 | // Sandbox Javascript imports 123 | const getAllEventData = require('getAllEventData'); 124 | const getType = require('getType'); 125 | const sendHttpRequest = require('sendHttpRequest'); 126 | const JSON = require('JSON'); 127 | const Math = require('Math'); 128 | const getTimestampMillis = require('getTimestampMillis'); 129 | const sha256Sync = require('sha256Sync'); 130 | const toBase64 = require('toBase64'); 131 | const fromBase64 = require('fromBase64'); 132 | const getCookieValues = require('getCookieValues'); 133 | const setCookie = require('setCookie'); 134 | const decodeUriComponent = require('decodeUriComponent'); 135 | const parseUrl = require('parseUrl'); 136 | const computeEffectiveTldPlusOne = require('computeEffectiveTldPlusOne'); 137 | 138 | // Constants 139 | const API_ENDPOINT = 'https://graph.facebook.com'; 140 | const API_VERSION = 'v16.0'; 141 | const PARTNER_AGENT = 'gtmss-1.0.0-0.0.9'; 142 | const GTM_EVENT_MAPPINGS = { 143 | "add_payment_info": "AddPaymentInfo", 144 | "add_to_cart": "AddToCart", 145 | "add_to_wishlist": "AddToWishlist", 146 | "gtm.dom": "PageView", 147 | "page_view": "PageView", 148 | "purchase": "Purchase", 149 | "search": "Search", 150 | "begin_checkout": "InitiateCheckout", 151 | "generate_lead": "Lead", 152 | "view_item": "ViewContent", 153 | "sign_up": "CompleteRegistration" 154 | }; 155 | 156 | function isAlreadyHashed(input){ 157 | return input && (input.match('^[A-Fa-f0-9]{64}$') != null); 158 | } 159 | 160 | function setFbCookie(name, value, expire) { 161 | setCookie(name, value, { 162 | domain: 'auto', 163 | path: '/', 164 | samesite: 'Lax', 165 | secure: true, 166 | 'max-age': expire || 7776000, // default to 90 days 167 | httpOnly: false 168 | }); 169 | } 170 | 171 | function setHttpOnlyCookie(name, value, expire) { 172 | setCookie(name, value, { 173 | domain: 'auto', 174 | path: '/', 175 | samesite: 'strict', 176 | secure: true, 177 | 'max-age': expire || 7776000, // default to 90 days 178 | httpOnly: true 179 | }); 180 | } 181 | 182 | function getFbcValue() { 183 | let fbc = eventModel['x-fb-ck-fbc'] || getCookieValues('_fbc', true)[0]; 184 | const url = eventModel.page_location; 185 | const subDomainIndex = url ? computeEffectiveTldPlusOne(url).split('.').length - 1 : 1; 186 | const parsedUrl = parseUrl(url); 187 | if (parsedUrl && parsedUrl.searchParams.fbclid) { 188 | fbc = 'fb.' + subDomainIndex + '.' + getTimestampMillis() + '.' + decodeUriComponent(parsedUrl.searchParams.fbclid); 189 | } 190 | 191 | return fbc; 192 | } 193 | 194 | function hashFunction(input){ 195 | const type = getType(input); 196 | if(type == 'undefined' || input == 'undefined') { 197 | return undefined; 198 | } 199 | 200 | if(input == null || isAlreadyHashed(input)){ 201 | return input; 202 | } 203 | 204 | return sha256Sync(input.trim().toLowerCase(), {outputEncoding: 'hex'}); 205 | } 206 | 207 | function getContentFromItems(items) { 208 | return items.map(item => { 209 | return { 210 | "id": (item.item_id || item.item_name) || undefined, 211 | "item_price": item.price || undefined, 212 | "quantity": item.quantity || undefined, 213 | }; 214 | }); 215 | } 216 | 217 | function getFacebookEventName(gtmEventName) { 218 | return GTM_EVENT_MAPPINGS[gtmEventName] || gtmEventName; 219 | } 220 | 221 | const eventModel = getAllEventData(); 222 | const event = {}; 223 | event.event_name = getFacebookEventName(eventModel.event_name); 224 | event.event_time = eventModel.event_time || (Math.round(getTimestampMillis() / 1000)); 225 | event.event_id = eventModel.event_id; 226 | event.event_source_url = eventModel.page_location; 227 | if(eventModel.action_source || data.actionSource) { 228 | event.action_source = eventModel.action_source ? eventModel.action_source : data.actionSource; 229 | } 230 | 231 | event.user_data = {}; 232 | // Default Tag Parameters 233 | event.user_data.client_ip_address = eventModel.ip_override; 234 | event.user_data.client_user_agent = eventModel.user_agent; 235 | 236 | 237 | // Commmon Event Schema Parameters 238 | event.user_data.em = eventModel['x-fb-ud-em'] || 239 | (eventModel.user_data != null ? hashFunction(eventModel.user_data.email_address) : undefined); 240 | 241 | let normalizedPhoneNumber = null; 242 | if (eventModel.user_data && eventModel.user_data.phone_number) { 243 | normalizedPhoneNumber = eventModel.user_data.phone_number.replace("+", "").replace("-", "").replace(" ", "").replace("(", "").replace(")", ""); 244 | normalizedPhoneNumber = hashFunction(normalizedPhoneNumber); 245 | } 246 | event.user_data.ph = eventModel['x-fb-ud-ph'] || (normalizedPhoneNumber != null ? normalizedPhoneNumber : undefined); 247 | 248 | const addressData = (eventModel.user_data != null && eventModel.user_data.address != null) ? eventModel.user_data.address : {}; 249 | event.user_data.fn = eventModel['x-fb-ud-fn'] || hashFunction(addressData.first_name); 250 | event.user_data.ln = eventModel['x-fb-ud-ln'] || hashFunction(addressData.last_name); 251 | event.user_data.ct = eventModel['x-fb-ud-ct'] || hashFunction(addressData.city); 252 | event.user_data.st = eventModel['x-fb-ud-st'] || hashFunction(addressData.region); 253 | event.user_data.zp = eventModel['x-fb-ud-zp'] || hashFunction(addressData.postal_code); 254 | event.user_data.country = eventModel['x-fb-ud-country'] || hashFunction(addressData.country); 255 | 256 | // Conversions API Specific Parameters 257 | event.user_data.ge = eventModel['x-fb-ud-ge']; 258 | event.user_data.db = eventModel['x-fb-ud-db']; 259 | event.user_data.external_id = eventModel['x-fb-ud-external_id']; 260 | event.user_data.subscription_id = eventModel['x-fb-ud-subscription_id']; 261 | event.user_data.fbp = eventModel['x-fb-ck-fbp'] || getCookieValues('_fbp', true)[0]; 262 | event.user_data.fbc = getFbcValue(); 263 | event.user_data.fb_login_id = eventModel['x-fb-ud-fb-login-id'] || (eventModel.user_data != null && eventModel.user_data.fb_login_id != null ? eventModel.user_data.fb_login_id : undefined); 264 | 265 | event.custom_data = {}; 266 | event.custom_data.currency = eventModel.currency; 267 | event.custom_data.value = eventModel.value; 268 | event.custom_data.search_string = eventModel.search_term; 269 | event.custom_data.order_id = eventModel.transaction_id; 270 | event.custom_data.content_category = eventModel['x-fb-cd-content_category']; 271 | event.custom_data.content_ids = eventModel['x-fb-cd-content_ids']; 272 | event.custom_data.content_name = eventModel['x-fb-cd-content_name']; 273 | event.custom_data.content_type = eventModel['x-fb-cd-content_type']; 274 | const invalidString = "[object Object]"; 275 | event.custom_data.contents = (eventModel['x-fb-cd-contents'] != null && eventModel['x-fb-cd-contents'].indexOf(invalidString) == 0 ? null : (typeof(eventModel['x-fb-cd-contents']) == "string" ? JSON.parse(eventModel['x-fb-cd-contents']) : eventModel['x-fb-cd-contents'])) || (eventModel.items != null ? getContentFromItems(eventModel.items) : undefined); 276 | 277 | const customProperties = (eventModel.custom_properties != null) ? (eventModel.custom_properties.indexOf(invalidString) == 0 ? null : (typeof(eventModel.custom_properties) == "string" ?JSON.parse(eventModel.custom_properties) : eventModel.custom_properties)) : {}; 278 | for (const property in customProperties) { 279 | event.custom_data[property] = customProperties[property]; 280 | } 281 | event.custom_data.num_items = eventModel['x-fb-cd-num_items']; 282 | event.custom_data.predicted_ltv = eventModel['x-fb-cd-predicted_ltv']; 283 | event.custom_data.status = eventModel['x-fb-cd-status']; 284 | event.custom_data.delivery_category = eventModel['x-fb-cd-delivery_category']; 285 | 286 | event.data_processing_options = eventModel.data_processing_options; 287 | event.data_processing_options_country = eventModel.data_processing_options_country; 288 | event.data_processing_options_state = eventModel.data_processing_options_state; 289 | 290 | function setGtmEecCookie(value) { 291 | const cookieJsonStr = JSON.stringify(value); 292 | 293 | const gtmeecCookieValueBase64 = toBase64(cookieJsonStr); 294 | 295 | setHttpOnlyCookie('_gtmeec', gtmeecCookieValueBase64); 296 | } 297 | 298 | //sets first party cookie with latest merged user data. 299 | function setResponseHeaderCookies(user_data) { 300 | let gtmeecCookie = JSON.parse('{}'); 301 | 302 | // if user_data has new information, gtmeec data is overriden 303 | if (user_data.em) { 304 | gtmeecCookie.em = user_data.em; 305 | } 306 | 307 | if (user_data.ph) { 308 | gtmeecCookie.ph = user_data.ph; 309 | } 310 | 311 | if (user_data.ln) { 312 | gtmeecCookie.ln = user_data.ln; 313 | } 314 | 315 | if (user_data.fn) { 316 | gtmeecCookie.fn = user_data.fn; 317 | } 318 | 319 | if (user_data.ct) { 320 | gtmeecCookie.ct = user_data.ct; 321 | } 322 | 323 | if (user_data.st) { 324 | gtmeecCookie.st = user_data.st; 325 | } 326 | 327 | if (user_data.zp) { 328 | gtmeecCookie.zp = user_data.zp; 329 | } 330 | 331 | if (user_data.ge) { 332 | gtmeecCookie.ge = user_data.ge; 333 | } 334 | 335 | if (user_data.db) { 336 | gtmeecCookie.db = user_data.db; 337 | } 338 | 339 | if (user_data.country) { 340 | gtmeecCookie.country = user_data.country; 341 | } 342 | 343 | if (user_data.external_id){ 344 | gtmeecCookie.external_id = user_data.external_id; 345 | } 346 | 347 | if (user_data.fb_login_id) { 348 | gtmeecCookie.fb_login_id = user_data.fb_login_id; 349 | } 350 | 351 | setGtmEecCookie(gtmeecCookie); 352 | } 353 | 354 | //enhance event data with first party `_gtmeec` cookie 355 | function enhanceEventData(user_data) { 356 | 357 | const cookieValues = getCookieValues('_gtmeec', true); 358 | 359 | if (!cookieValues) { 360 | return user_data; 361 | } 362 | 363 | if (cookieValues.length == 0) { 364 | return user_data; 365 | } 366 | 367 | const encodedValue = cookieValues[0]; 368 | 369 | if (!encodedValue) { 370 | return user_data; 371 | } 372 | 373 | const jsonStr = fromBase64(encodedValue); 374 | if (!jsonStr) { 375 | return user_data; 376 | } 377 | 378 | const gtmeecData = JSON.parse(jsonStr); 379 | 380 | // if incoming event has already have the customer information then don't change 381 | if (gtmeecData) { 382 | if (!user_data.em && gtmeecData.em) { 383 | user_data.em = gtmeecData.em; 384 | } 385 | 386 | if (!user_data.ph && gtmeecData.ph) { 387 | user_data.ph = gtmeecData.ph; 388 | } 389 | 390 | if (!user_data.ln && gtmeecData.ln) { 391 | user_data.ln = gtmeecData.ln; 392 | } 393 | 394 | if (!user_data.fn && gtmeecData.fn) { 395 | user_data.fn = gtmeecData.fn; 396 | } 397 | 398 | if (!user_data.ct && gtmeecData.ct) { 399 | user_data.ct = gtmeecData.ct; 400 | } 401 | 402 | if (!user_data.st && gtmeecData.st) { 403 | user_data.st = gtmeecData.st; 404 | } 405 | 406 | if (!user_data.zp && gtmeecData.zp) { 407 | user_data.zp = gtmeecData.zp; 408 | } 409 | 410 | if (!user_data.ge && gtmeecData.ge) { 411 | user_data.ge = gtmeecData.ge; 412 | } 413 | 414 | if (!user_data.db && gtmeecData.db) { 415 | user_data.db = gtmeecData.db; 416 | } 417 | 418 | if (!user_data.country && gtmeecData.country) { 419 | user_data.country = gtmeecData.country; 420 | } 421 | 422 | if (!user_data.external_id && gtmeecData.external_id) { 423 | user_data.external_id = gtmeecData.external_id; 424 | } 425 | 426 | if (!user_data.fb_login_id && gtmeecData.fb_login_id) { 427 | user_data.fb_login_id = gtmeecData.fb_login_id; 428 | } 429 | } 430 | 431 | return user_data; 432 | } 433 | 434 | //send events to CAPI Server 435 | function sendEventToCapiServers(pixel_event) { 436 | 437 | // if event enhancement is enabled then event data is enhanced 438 | let partnerAgent = PARTNER_AGENT; 439 | if (data.enableEventEnhancement) { 440 | pixel_event.user_data = enhanceEventData(pixel_event.user_data); 441 | partnerAgent = PARTNER_AGENT + '-ee'; 442 | } 443 | 444 | const eventRequest = {data: [pixel_event], partner_agent: partnerAgent}; 445 | 446 | if(eventModel.test_event_code || data.testEventCode) { 447 | eventRequest.test_event_code = eventModel.test_event_code ? eventModel.test_event_code : data.testEventCode; 448 | } 449 | 450 | const routeParams = 'events?access_token=' + data.apiAccessToken; 451 | const graphEndpoint = [API_ENDPOINT, 452 | API_VERSION, 453 | data.pixelId, 454 | routeParams].join('/'); 455 | 456 | const requestHeaders = {headers: {'content-type': 'application/json'}, method: 'POST'}; 457 | return sendHttpRequest( 458 | graphEndpoint, 459 | (statusCode, headers, response) => { 460 | if (statusCode >= 200 && statusCode < 300) { 461 | 462 | if (data.extendCookies && pixel_event.user_data.fbc) { 463 | setFbCookie('_fbc', pixel_event.user_data.fbc); 464 | } 465 | 466 | if (data.extendCookies && pixel_event.user_data.fbp) { 467 | setFbCookie('_fbp', pixel_event.user_data.fbp); 468 | } 469 | 470 | if (data.enableEventEnhancement) { 471 | setResponseHeaderCookies(pixel_event.user_data); 472 | } 473 | 474 | data.gtmOnSuccess(); 475 | } else { 476 | data.gtmOnFailure(); 477 | } 478 | }, 479 | requestHeaders, 480 | JSON.stringify(eventRequest) 481 | ); 482 | } 483 | 484 | sendEventToCapiServers(event); 485 | 486 | 487 | ___SERVER_PERMISSIONS___ 488 | 489 | [ 490 | { 491 | "instance": { 492 | "key": { 493 | "publicId": "read_event_data", 494 | "versionId": "1" 495 | }, 496 | "param": [ 497 | { 498 | "key": "eventDataAccess", 499 | "value": { 500 | "type": 1, 501 | "string": "any" 502 | } 503 | } 504 | ] 505 | }, 506 | "clientAnnotations": { 507 | "isEditedByUser": true 508 | }, 509 | "isRequired": true 510 | }, 511 | { 512 | "instance": { 513 | "key": { 514 | "publicId": "send_http", 515 | "versionId": "1" 516 | }, 517 | "param": [ 518 | { 519 | "key": "allowedUrls", 520 | "value": { 521 | "type": 1, 522 | "string": "specific" 523 | } 524 | }, 525 | { 526 | "key": "urls", 527 | "value": { 528 | "type": 2, 529 | "listItem": [ 530 | { 531 | "type": 1, 532 | "string": "https://graph.facebook.com/" 533 | } 534 | ] 535 | } 536 | } 537 | ] 538 | }, 539 | "clientAnnotations": { 540 | "isEditedByUser": true 541 | }, 542 | "isRequired": true 543 | }, 544 | { 545 | "instance": { 546 | "key": { 547 | "publicId": "get_cookies", 548 | "versionId": "1" 549 | }, 550 | "param": [ 551 | { 552 | "key": "cookieAccess", 553 | "value": { 554 | "type": 1, 555 | "string": "specific" 556 | } 557 | }, 558 | { 559 | "key": "cookieNames", 560 | "value": { 561 | "type": 2, 562 | "listItem": [ 563 | { 564 | "type": 1, 565 | "string": "_fbp" 566 | }, 567 | { 568 | "type": 1, 569 | "string": "_fbc" 570 | }, 571 | { 572 | "type": 1, 573 | "string": "_gtmeec" 574 | } 575 | ] 576 | } 577 | } 578 | ] 579 | }, 580 | "clientAnnotations": { 581 | "isEditedByUser": true 582 | }, 583 | "isRequired": true 584 | }, 585 | { 586 | "instance": { 587 | "key": { 588 | "publicId": "set_cookies", 589 | "versionId": "1" 590 | }, 591 | "param": [ 592 | { 593 | "key": "allowedCookies", 594 | "value": { 595 | "type": 2, 596 | "listItem": [ 597 | { 598 | "type": 3, 599 | "mapKey": [ 600 | { 601 | "type": 1, 602 | "string": "name" 603 | }, 604 | { 605 | "type": 1, 606 | "string": "domain" 607 | }, 608 | { 609 | "type": 1, 610 | "string": "path" 611 | }, 612 | { 613 | "type": 1, 614 | "string": "secure" 615 | }, 616 | { 617 | "type": 1, 618 | "string": "session" 619 | } 620 | ], 621 | "mapValue": [ 622 | { 623 | "type": 1, 624 | "string": "_fbc" 625 | }, 626 | { 627 | "type": 1, 628 | "string": "*" 629 | }, 630 | { 631 | "type": 1, 632 | "string": "*" 633 | }, 634 | { 635 | "type": 1, 636 | "string": "any" 637 | }, 638 | { 639 | "type": 1, 640 | "string": "any" 641 | } 642 | ] 643 | }, 644 | { 645 | "type": 3, 646 | "mapKey": [ 647 | { 648 | "type": 1, 649 | "string": "name" 650 | }, 651 | { 652 | "type": 1, 653 | "string": "domain" 654 | }, 655 | { 656 | "type": 1, 657 | "string": "path" 658 | }, 659 | { 660 | "type": 1, 661 | "string": "secure" 662 | }, 663 | { 664 | "type": 1, 665 | "string": "session" 666 | } 667 | ], 668 | "mapValue": [ 669 | { 670 | "type": 1, 671 | "string": "_fbp" 672 | }, 673 | { 674 | "type": 1, 675 | "string": "*" 676 | }, 677 | { 678 | "type": 1, 679 | "string": "*" 680 | }, 681 | { 682 | "type": 1, 683 | "string": "any" 684 | }, 685 | { 686 | "type": 1, 687 | "string": "any" 688 | } 689 | ] 690 | }, 691 | { 692 | "type": 3, 693 | "mapKey": [ 694 | { 695 | "type": 1, 696 | "string": "name" 697 | }, 698 | { 699 | "type": 1, 700 | "string": "domain" 701 | }, 702 | { 703 | "type": 1, 704 | "string": "path" 705 | }, 706 | { 707 | "type": 1, 708 | "string": "secure" 709 | }, 710 | { 711 | "type": 1, 712 | "string": "session" 713 | } 714 | ], 715 | "mapValue": [ 716 | { 717 | "type": 1, 718 | "string": "_gtmeec" 719 | }, 720 | { 721 | "type": 1, 722 | "string": "*" 723 | }, 724 | { 725 | "type": 1, 726 | "string": "*" 727 | }, 728 | { 729 | "type": 1, 730 | "string": "secure" 731 | }, 732 | { 733 | "type": 1, 734 | "string": "any" 735 | } 736 | ] 737 | } 738 | ] 739 | } 740 | } 741 | ] 742 | }, 743 | "clientAnnotations": { 744 | "isEditedByUser": true 745 | }, 746 | "isRequired": true 747 | } 748 | ] 749 | 750 | 751 | ___TESTS___ 752 | 753 | scenarios: 754 | - name: on EventModel model data tag triggers to send to Conversions API 755 | code: |- 756 | // Act 757 | runCode(testConfigurationData); 758 | 759 | //Assert 760 | assertApi('sendHttpRequest').wasCalledWith(requestEndpoint, actualSuccessCallback, requestHeaderOptions, JSON.stringify(requestData)); 761 | assertApi('gtmOnSuccess').wasCalled(); 762 | - name: on Event with common event schema triggers tag to send to Conversions API 763 | code: |- 764 | const preTagFireEventTime = Math.round(getTimestampMillis() / 1000); 765 | const common_event_schema = { 766 | event_name: testData.event_name, 767 | client_id: 'client123', 768 | ip_override: testData.ip_address, 769 | user_agent: testData.user_agent, 770 | }; 771 | mock('getAllEventData', () => { 772 | return common_event_schema; 773 | }); 774 | 775 | // Act 776 | runCode(testConfigurationData); 777 | 778 | //Assert 779 | const actualTagFireEventTime = JSON.parse(httpBody).data[0].event_time; 780 | assertThat(actualTagFireEventTime-preTagFireEventTime).isLessThan(1); 781 | assertApi('gtmOnSuccess').wasCalled(); 782 | - name: on sending action source from Client, Tag overrides the preset configuration 783 | code: |- 784 | // Act 785 | mock('getAllEventData', () => { 786 | inputEventModel.action_source = testData.action_source; 787 | return inputEventModel; 788 | }); 789 | runCode(testConfigurationData); 790 | 791 | //Assert 792 | assertThat(JSON.parse(httpBody).data[0].action_source).isEqualTo(inputEventModel.action_source); 793 | - name: on receiving event, if GTM Standard Event then Tag converts to corresponding 794 | Conversions API Event, passes through as-is if otherwise 795 | code: |- 796 | // Act 797 | mock('getAllEventData', () => { 798 | inputEventModel.event_name = 'add_to_wishlist'; 799 | return inputEventModel; 800 | }); 801 | runCode(testConfigurationData); 802 | 803 | //Assert 804 | assertThat(JSON.parse(httpBody).data[0].event_name).isEqualTo('AddToWishlist'); 805 | 806 | 807 | // Act 808 | mock('getAllEventData', () => { 809 | inputEventModel.event_name = 'custom_event'; 810 | return inputEventModel; 811 | }); 812 | runCode(testConfigurationData); 813 | 814 | //Assert 815 | assertThat(JSON.parse(httpBody).data[0].event_name).isEqualTo('custom_event'); 816 | 817 | // Act 818 | mock('getAllEventData', () => { 819 | inputEventModel.event_name = 'generate_lead'; 820 | return inputEventModel; 821 | }); 822 | runCode(testConfigurationData); 823 | 824 | //Assert 825 | assertThat(JSON.parse(httpBody).data[0].event_name).isEqualTo('Lead'); 826 | - name: On receiving event, hashes the the user_data fields if they are not already 827 | hashed 828 | code: |- 829 | // Un-hashed raw email_address from Common Event Schema is hashed before posted to Conversions API. 830 | 831 | // Act 832 | mock('getAllEventData', () => { 833 | inputEventModel = {}; 834 | inputEventModel['x-fb-ud-em'] = null; 835 | inputEventModel['x-fb-ud-ph'] = null; 836 | inputEventModel['x-fb-ud-fn'] = null; 837 | inputEventModel['x-fb-ud-ln'] = null; 838 | inputEventModel['x-fb-ud-ct'] = null; 839 | inputEventModel['x-fb-ud-st'] = null; 840 | inputEventModel['x-fb-ud-zp'] = null; 841 | inputEventModel['x-fb-ud-country'] = null; 842 | inputEventModel.user_data = {}; 843 | inputEventModel.user_data.email_address = 'foo@bar.com'; 844 | inputEventModel.user_data.phone_number = '1234567890'; 845 | inputEventModel.user_data.address = {}; 846 | inputEventModel.user_data.address.first_name = 'Foo'; 847 | inputEventModel.user_data.address.last_name = 'Bar'; 848 | inputEventModel.user_data.address.city = 'Menlo Park'; 849 | inputEventModel.user_data.address.region = 'ca'; 850 | inputEventModel.user_data.address.postal_code = '12345'; 851 | inputEventModel.user_data.address.country = 'usa'; 852 | return inputEventModel; 853 | }); 854 | runCode(testConfigurationData); 855 | 856 | //Assert 857 | assertThat(JSON.parse(httpBody).data[0].user_data.em).isEqualTo(hashFunction('foo@bar.com')); 858 | assertThat(JSON.parse(httpBody).data[0].user_data.ph).isEqualTo(hashFunction('1234567890')); 859 | assertThat(JSON.parse(httpBody).data[0].user_data.fn).isEqualTo(hashFunction('Foo')); 860 | assertThat(JSON.parse(httpBody).data[0].user_data.ln).isEqualTo(hashFunction('Bar')); 861 | assertThat(JSON.parse(httpBody).data[0].user_data.ct).isEqualTo(hashFunction('Menlo Park')); 862 | assertThat(JSON.parse(httpBody).data[0].user_data.st).isEqualTo(hashFunction('ca')); 863 | assertThat(JSON.parse(httpBody).data[0].user_data.zp).isEqualTo(hashFunction('12345')); 864 | assertThat(JSON.parse(httpBody).data[0].user_data.country).isEqualTo(hashFunction('usa')); 865 | 866 | // Un-hashed raw email_address in mixed-case is converted to lowercase, hashed and posted to Conversions API. 867 | 868 | // Act 869 | mock('getAllEventData', () => { 870 | inputEventModel.user_data.email_address = 'FOO@BAR.com'; 871 | return inputEventModel; 872 | }); 873 | runCode(testConfigurationData); 874 | 875 | //Assert 876 | assertThat(JSON.parse(httpBody).data[0].user_data.em).isEqualTo(hashFunction('foo@bar.com')); 877 | 878 | 879 | // Already sha256(email_address) field from GA4 schema, is unchanged, is posted as-is to Conversions API. 880 | 881 | // Act 882 | mock('getAllEventData', () => { 883 | inputEventModel.user_data.email_address = hashFunction('foo@bar.com'); 884 | return inputEventModel; 885 | }); 886 | runCode(testConfigurationData); 887 | 888 | //Assert 889 | assertThat(JSON.parse(httpBody).data[0].user_data.em).isEqualTo(hashFunction('foo@bar.com')); 890 | 891 | // Already null email field from GA4 schema, is sent as null to Conversions API. 892 | 893 | // Act 894 | mock('getAllEventData', () => { 895 | inputEventModel = {}; 896 | inputEventModel.user_data = {}; 897 | inputEventModel.user_data.email_address = null; 898 | return inputEventModel; 899 | }); 900 | runCode(testConfigurationData); 901 | 902 | //Assert 903 | assertThat(JSON.parse(httpBody).data[0].user_data.em).isNull(); 904 | - name: On receiving event with fbp/fbc cookies, it is sent to Conversions API 905 | code: |- 906 | // Act 907 | mock('getAllEventData', () => { 908 | inputEventModel['x-fb-ck-fbp'] = null; 909 | inputEventModel['x-fb-ck-fbc'] = null; 910 | return inputEventModel; 911 | }); 912 | 913 | mock('getCookieValues', (cookieName) => { 914 | if(cookieName === '_fbp') return ['fbp_cookie']; 915 | if(cookieName === '_fbc') return ['fbc_cookie']; 916 | }); 917 | 918 | runCode(testConfigurationData); 919 | 920 | //Assert 921 | assertThat(JSON.parse(httpBody).data[0].user_data.fbp).isEqualTo('fbp_cookie'); 922 | assertThat(JSON.parse(httpBody).data[0].user_data.fbc).isEqualTo('fbc_cookie'); 923 | - name: On receiving GA4 event, with the items info, tag parses them into Conversions 924 | API schema 925 | code: |- 926 | // Act 927 | let items = [ 928 | { 929 | item_id: '1', 930 | quantity: 5, 931 | price: 123.45, 932 | }, 933 | { 934 | item_id: '2', 935 | quantity: 10, 936 | price: 123.45, 937 | } 938 | ]; 939 | 940 | mock('getAllEventData', () => { 941 | inputEventModel = {}; 942 | inputEventModel['x-fb-cd-contents'] = null; 943 | inputEventModel.items = items; 944 | return inputEventModel; 945 | }); 946 | runCode(testConfigurationData); 947 | 948 | //Assert 949 | let actual_contents = JSON.parse(httpBody).data[0].custom_data.contents; 950 | assertThat(JSON.parse(httpBody).data[0].custom_data.contents.length).isEqualTo(items.length); 951 | for( var i = 0; i < items.length; i++) { 952 | assertThat(actual_contents[i].id).isEqualTo(items[i].item_id); 953 | assertThat(actual_contents[i].item_price).isEqualTo(items[i].price); 954 | assertThat(actual_contents[i].quantity).isEqualTo(items[i].quantity); 955 | } 956 | 957 | // Act 958 | mock('getAllEventData', () => { 959 | inputEventModel = {}; 960 | inputEventModel.items = null; 961 | return inputEventModel; 962 | }); 963 | runCode(testConfigurationData); 964 | 965 | //Assert 966 | assertThat(JSON.parse(httpBody).data[0].custom_data.contents).isUndefined(); 967 | - name: When address is missing it skips parsing the nested fields 968 | code: | 969 | mock('getAllEventData', () => { 970 | inputEventModel['x-fb-ud-em'] = null; 971 | inputEventModel['x-fb-ud-ph'] = null; 972 | inputEventModel['x-fb-ud-fn'] = null; 973 | inputEventModel['x-fb-ud-ln'] = null; 974 | inputEventModel['x-fb-ud-ct'] = null; 975 | inputEventModel['x-fb-ud-st'] = null; 976 | inputEventModel['x-fb-ud-zp'] = null; 977 | inputEventModel['x-fb-ud-country'] = null; 978 | inputEventModel.user_data = {}; 979 | inputEventModel.user_data.email_address = 'foo@bar.com'; 980 | inputEventModel.user_data.phone_number = '1234567890'; 981 | return inputEventModel; 982 | }); 983 | 984 | runCode(testConfigurationData); 985 | 986 | assertThat(JSON.parse(httpBody).data[0].user_data.em).isEqualTo(hashFunction('foo@bar.com')); 987 | assertThat(JSON.parse(httpBody).data[0].user_data.ph).isEqualTo(hashFunction('1234567890')); 988 | assertThat(JSON.parse(httpBody).data[0].user_data.fn).isUndefined(); 989 | assertThat(JSON.parse(httpBody).data[0].user_data.ln).isUndefined(); 990 | assertThat(JSON.parse(httpBody).data[0].user_data.ct).isUndefined(); 991 | assertThat(JSON.parse(httpBody).data[0].user_data.st).isUndefined(); 992 | assertThat(JSON.parse(httpBody).data[0].user_data.zp).isUndefined(); 993 | assertThat(JSON.parse(httpBody).data[0].user_data.country).isUndefined(); 994 | - name: When parameters are undefined skip parsing 995 | code: | 996 | mock('getAllEventData', () => { 997 | inputEventModel = {}; 998 | inputEventModel['x-fb-ud-em'] = null; 999 | inputEventModel['x-fb-ud-ph'] = null; 1000 | inputEventModel['x-fb-ud-fn'] = null; 1001 | inputEventModel['x-fb-ud-ln'] = null; 1002 | inputEventModel['x-fb-ud-ct'] = null; 1003 | inputEventModel['x-fb-ud-st'] = null; 1004 | inputEventModel['x-fb-ud-zp'] = null; 1005 | inputEventModel['x-fb-ud-country'] = null; 1006 | inputEventModel['x-fb-ud-fb-login-id'] = null; 1007 | inputEventModel.user_data = {}; 1008 | inputEventModel.user_data.email_address = undefined; 1009 | inputEventModel.user_data.phone_number = '1234567890'; 1010 | inputEventModel.user_data.address = {}; 1011 | inputEventModel.user_data.address.first_name = 'John'; 1012 | inputEventModel.user_data.address.last_name = undefined; 1013 | inputEventModel.user_data.address.city = 'menlopark'; 1014 | inputEventModel.user_data.address.region = 'ca'; 1015 | inputEventModel.user_data.address.postal_code = '94025'; 1016 | inputEventModel.user_data.address.country = 'usa'; 1017 | inputEventModel.user_data.fb_login_id = 123456789; 1018 | return inputEventModel; 1019 | }); 1020 | 1021 | runCode(testConfigurationData); 1022 | 1023 | assertThat(JSON.parse(httpBody).data[0].user_data.em).isUndefined(); 1024 | assertThat(JSON.parse(httpBody).data[0].user_data.ph).isEqualTo(hashFunction('1234567890')); 1025 | assertThat(JSON.parse(httpBody).data[0].user_data.fn).isEqualTo(hashFunction('John')); 1026 | assertThat(JSON.parse(httpBody).data[0].user_data.ln).isUndefined(); 1027 | assertThat(JSON.parse(httpBody).data[0].user_data.ct).isEqualTo(hashFunction('menlopark')); 1028 | assertThat(JSON.parse(httpBody).data[0].user_data.st).isEqualTo(hashFunction('ca')); 1029 | assertThat(JSON.parse(httpBody).data[0].user_data.zp).isEqualTo(hashFunction('94025')); 1030 | assertThat(JSON.parse(httpBody).data[0].user_data.country).isEqualTo(hashFunction('usa')); 1031 | assertThat(JSON.parse(httpBody).data[0].user_data.fb_login_id).isEqualTo(123456789); 1032 | - name: Set Meta cookies (fbp / fbc) if 'extendCookies' checkbox is ticked 1033 | code: | 1034 | runCode({ 1035 | pixelId: '123', 1036 | apiAccessToken: 'abc', 1037 | testEventCode: 'test123', 1038 | actionSource: 'source123', 1039 | extendCookies: true 1040 | }); 1041 | 1042 | //Assert 1043 | assertApi('setCookie').wasCalled(); 1044 | assertApi('gtmOnSuccess').wasCalled(); 1045 | - name: Do not set Meta cookies (fbp / fbc) if 'extendCookies' checkbox is ticked 1046 | code: | 1047 | runCode({ 1048 | pixelId: '123', 1049 | apiAccessToken: 'abc', 1050 | testEventCode: 'test123', 1051 | actionSource: 'source123', 1052 | extendCookies: false 1053 | }); 1054 | 1055 | //Assert 1056 | assertApi('setCookie').wasNotCalled(); 1057 | assertApi('gtmOnSuccess').wasCalled(); 1058 | - name: On receiving event, sets the data_processing_options field if present 1059 | code: | 1060 | mock('getAllEventData', () => { 1061 | inputEventModel.data_processing_options = testData.data_processing_options; 1062 | inputEventModel.data_processing_options_country = testData.data_processing_options_country; 1063 | inputEventModel.data_processing_options_state = testData.data_processing_options_state; 1064 | return inputEventModel; 1065 | }); 1066 | runCode(testConfigurationData); 1067 | 1068 | //Assert 1069 | assertThat(JSON.parse(httpBody).data[0].data_processing_options).isEqualTo(inputEventModel.data_processing_options); 1070 | assertThat(JSON.parse(httpBody).data[0].data_processing_options_country).isEqualTo(inputEventModel.data_processing_options_country); 1071 | assertThat(JSON.parse(httpBody).data[0].data_processing_options_state).isEqualTo(inputEventModel.data_processing_options_state); 1072 | - name: Set Event Enhancement Cookie (gtmeec) if `enableEventEnhancement` is ticked 1073 | code: |- 1074 | mock('getAllEventData', () => { 1075 | inputEventModel = {}; 1076 | inputEventModel.event_name = 'purchase'; 1077 | inputEventModel.user_data = {}; 1078 | inputEventModel.user_data.email_address = 'foo@bar.com'; 1079 | inputEventModel.user_data.phone_number = '1234567890'; 1080 | return inputEventModel; 1081 | }); 1082 | 1083 | runCode(testConfigurationData); 1084 | 1085 | runCode({ 1086 | pixelId: '123', 1087 | apiAccessToken: 'abc', 1088 | testEventCode: 'test123', 1089 | actionSource: 'source123', 1090 | enableEventEnhancement: true, 1091 | extendCookies: false 1092 | }); 1093 | 1094 | let cookieOptions = { 1095 | domain: 'auto', 1096 | path: '/', 1097 | samesite: 'strict', 1098 | secure: true, 1099 | 'max-age': 7776000, // default to 90 days 1100 | httpOnly: true 1101 | }; 1102 | 1103 | //Assert 1104 | assertApi('getCookieValues').wasCalledWith('_gtmeec', true); 1105 | assertApi('setCookie').wasCalledWith('_gtmeec', 'eyJlbSI6IjBjN2U2YTQwNTg2MmU0MDJlYjc2YTcwZjhhMjZmYzczMmQwN2MzMjkzMWU5ZmFlOWFiMTU4MjkxMWQyZThhM2IiLCJwaCI6ImM3NzVlN2I3NTdlZGU2MzBjZDBhYTExMTNiZDEwMjY2MWFiMzg4MjljYTUyYTY0MjJhYjc4Mjg2MmYyNjg2NDYifQ==', cookieOptions); 1106 | assertApi('gtmOnSuccess').wasCalled(); 1107 | - name: Do not set Event Enhancement Cookie (gtmeec) if `enableEventEnhancement` is 1108 | not ticked 1109 | code: |- 1110 | runCode({ 1111 | pixelId: '123', 1112 | apiAccessToken: 'abc', 1113 | testEventCode: 'test123', 1114 | actionSource: 'source123', 1115 | extendCookies: false, 1116 | enableEventEnhancement: false 1117 | }); 1118 | 1119 | //Assert 1120 | assertApi('getCookieValues').wasNotCalledWith('_gtmeec', true); 1121 | assertApi('setCookie').wasNotCalled(); 1122 | assertApi('gtmOnSuccess').wasCalled(); 1123 | - name: Parse gtmeec Cookie and Enrich Event When `enableEventEnhancement` is ticked 1124 | code: | 1125 | mock('getAllEventData', () => { 1126 | inputEventModel = {}; 1127 | inputEventModel.event_name = 'purchase'; 1128 | inputEventModel.user_data = {}; 1129 | return inputEventModel; 1130 | }); 1131 | 1132 | runCode(testConfigurationData); 1133 | 1134 | const cookieName = '_gtmeec'; 1135 | const val = true; 1136 | 1137 | mock('getCookieValues', (cookieName, val) => { 1138 | return ['eyJlbSI6ImVlMjc4OTQzZGU4NGU1ZDYyNDM1NzhlZTFhMTA1N2JjY2UwZTUwZGFhZDk3NTVmNDVkZmE2NGI2MGIxM2JjNWQiLCJwaCI6ImM3NzVlN2I3NTdlZGU2MzBjZDBhYTExMTNiZDEwMjY2MWFiMzg4MjljYTUyYTY0MjJhYjc4Mjg2MmYyNjg2NDYifQ==']; 1139 | }); 1140 | 1141 | runCode({ 1142 | pixelId: '123', 1143 | apiAccessToken: 'abc', 1144 | testEventCode: 'test123', 1145 | actionSource: 'source123', 1146 | enableEventEnhancement: true, 1147 | extendCookies: false 1148 | }); 1149 | 1150 | let cookieOptions = { 1151 | domain: 'auto', 1152 | path: '/', 1153 | samesite: 'strict', 1154 | secure: true, 1155 | 'max-age': 7776000, // default to 90 days 1156 | httpOnly: true 1157 | }; 1158 | 1159 | // Assert 1160 | assertApi('getCookieValues').wasCalledWith('_gtmeec', true); 1161 | assertApi('setCookie').wasCalledWith('_gtmeec', 'eyJlbSI6ImVlMjc4OTQzZGU4NGU1ZDYyNDM1NzhlZTFhMTA1N2JjY2UwZTUwZGFhZDk3NTVmNDVkZmE2NGI2MGIxM2JjNWQiLCJwaCI6ImM3NzVlN2I3NTdlZGU2MzBjZDBhYTExMTNiZDEwMjY2MWFiMzg4MjljYTUyYTY0MjJhYjc4Mjg2MmYyNjg2NDYifQ==', cookieOptions); 1162 | assertApi('gtmOnSuccess').wasCalled(); 1163 | 1164 | assertThat(JSON.parse(httpBody).data[0].user_data.em).isEqualTo('ee278943de84e5d6243578ee1a1057bcce0e50daad9755f45dfa64b60b13bc5d'); 1165 | assertThat(JSON.parse(httpBody).data[0].user_data.ph).isEqualTo('c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646'); 1166 | setup: |- 1167 | // Arrange 1168 | const JSON = require('JSON'); 1169 | const Math = require('Math'); 1170 | const getTimestampMillis = require('getTimestampMillis'); 1171 | const sha256Sync = require('sha256Sync'); 1172 | 1173 | // helper methods 1174 | function hashFunction(input) { 1175 | return sha256Sync(input.trim().toLowerCase(), {outputEncoding: 'hex'}); 1176 | } 1177 | 1178 | const testConfigurationData = { 1179 | pixelId: '123', 1180 | apiAccessToken: 'abc', 1181 | testEventCode: 'test123', 1182 | actionSource: 'source123' 1183 | }; 1184 | 1185 | const testData = { 1186 | event_name: "Test1", 1187 | event_time: "123456789", 1188 | test_event_code: "test123", 1189 | action_source: 'website', 1190 | user_data: { 1191 | ip_address: '1.2.3.4', 1192 | user_agent: 'Test_UA', 1193 | email: 'test@example.com', 1194 | phone_number: '123456789', 1195 | first_name: 'foo', 1196 | last_name: 'bar', 1197 | gender: 'm', 1198 | date_of_brith: '19910526', 1199 | city: 'menlopark', 1200 | state: 'ca', 1201 | country: 'us', 1202 | zip: '12345', 1203 | external_id: 'user123', 1204 | subscription_id: 'abc123', 1205 | fbp: 'test_browser_id', 1206 | fbc: 'test_click_id', 1207 | fb_login_id: 123456789, 1208 | }, 1209 | custom_data: { 1210 | currency: 'USD', 1211 | value: '123', 1212 | search_string: 'query123', 1213 | transaction_id: 'order_123', 1214 | content_category: 'testCategory', 1215 | content_ids: ['123', '456'], 1216 | content_name: 'Foo', 1217 | content_type: 'product', 1218 | contents: [{'id': '123', 'quantity': 2}, {'id': '456', 'quantity': 2}], 1219 | num_items: '4', 1220 | predicted_ltv: '10000', 1221 | delivery_category: 'home_delivery', 1222 | status: 'subscribed', 1223 | }, 1224 | "data_processing_options": ["LDU"], 1225 | "data_processing_options_country": 1, 1226 | "data_processing_options_state": 1000, 1227 | }; 1228 | 1229 | let inputEventModel = { 1230 | 'event_name': testData.event_name, 1231 | 'event_time': testData.event_time, 1232 | 'ip_override': testData.user_data.ip_address, 1233 | 'user_agent': testData.user_data.user_agent, 1234 | 'test_event_code': testData.test_event_code, 1235 | 'x-fb-ud-em': testData.user_data.email, 1236 | 'x-fb-ud-ph': testData.user_data.phone_number, 1237 | 'x-fb-ud-fn': testData.user_data.first_name, 1238 | 'x-fb-ud-ln': testData.user_data.last_name, 1239 | 'x-fb-ud-ge': testData.user_data.gender, 1240 | 'x-fb-ud-db': testData.user_data.date_of_brith, 1241 | 'x-fb-ud-ct': testData.user_data.city, 1242 | 'x-fb-ud-st': testData.user_data.state, 1243 | 'x-fb-ud-zp': testData.user_data.zip, 1244 | 'x-fb-ud-country': testData.user_data.country, 1245 | 'x-fb-ud-external_id': testData.user_data.external_id, 1246 | 'x-fb-ud-subscription_id': testData.user_data.subscription_id, 1247 | 'x-fb-ud-fb-login-id': testData.user_data.fb_login_id, 1248 | 'x-fb-ck-fbp': testData.user_data.fbp, 1249 | 'x-fb-ck-fbc': testData.user_data.fbc, 1250 | 'currency': testData.custom_data.currency, 1251 | 'value': testData.custom_data.value, 1252 | 'search_term': testData.custom_data.search_string, 1253 | 'transaction_id': testData.custom_data.transaction_id, 1254 | 'x-fb-cd-status': testData.custom_data.status, 1255 | 'x-fb-cd-content_category': testData.custom_data.content_category, 1256 | 'x-fb-cd-content_name': testData.custom_data.content_name, 1257 | 'x-fb-cd-content_type': testData.custom_data.content_type, 1258 | 'x-fb-cd-contents': testData.custom_data.contents, 1259 | 'x-fb-cd-num_items': testData.custom_data.num_items, 1260 | 'x-fb-cd-predicted_ltv': testData.custom_data.predicted_ltv, 1261 | 'x-fb-cd-delivery_category': testData.custom_data.delivery_category, 1262 | 'data_processing_options': testData.data_processing_options, 1263 | 'data_processing_options_country': testData.data_processing_options_country, 1264 | 'data_processing_options_state': testData.data_processing_options_state, 1265 | }; 1266 | 1267 | const expectedEventData = { 1268 | 'event_name': testData.event_name, 1269 | 'event_time': testData.event_time, 1270 | 'action_source': testConfigurationData.actionSource, 1271 | 'user_data': { 1272 | 'client_ip_address': testData.user_data.ip_address, 1273 | 'client_user_agent': testData.user_data.user_agent, 1274 | 'em': testData.user_data.email, 1275 | 'ph': testData.user_data.phone_number, 1276 | 'fn': testData.user_data.first_name, 1277 | 'ln': testData.user_data.last_name, 1278 | 'ct': testData.user_data.city, 1279 | 'st': testData.user_data.state, 1280 | 'zp': testData.user_data.zip, 1281 | 'country': testData.user_data.country, 1282 | 'ge': testData.user_data.gender, 1283 | 'db': testData.user_data.date_of_brith, 1284 | 'external_id': testData.user_data.external_id, 1285 | 'subscription_id': testData.user_data.subscription_id, 1286 | 'fbp': testData.user_data.fbp, 1287 | 'fbc': testData.user_data.fbc, 1288 | 'fb_login_id': testData.user_data.fb_login_id, 1289 | }, 1290 | 'custom_data': { 1291 | 'currency': testData.custom_data.currency, 1292 | 'value': testData.custom_data.value, 1293 | 'search_string': testData.custom_data.search_string, 1294 | 'order_id': testData.custom_data.transaction_id, 1295 | 'content_category': testData.custom_data.content_category, 1296 | 'content_name': testData.custom_data.content_name, 1297 | 'content_type': testData.custom_data.content_type, 1298 | 'contents': testData.custom_data.contents, 1299 | 'num_items': testData.custom_data.num_items, 1300 | 'predicted_ltv': testData.custom_data.predicted_ltv, 1301 | 'status': testData.custom_data.status, 1302 | 'delivery_category': testData.custom_data.delivery_category, 1303 | }, 1304 | 'data_processing_options': testData.data_processing_options, 1305 | 'data_processing_options_country': testData.data_processing_options_country, 1306 | 'data_processing_options_state': testData.data_processing_options_state, 1307 | }; 1308 | 1309 | mock('getAllEventData', () => { 1310 | return inputEventModel; 1311 | }); 1312 | 1313 | const apiEndpoint = 'https://graph.facebook.com'; 1314 | const apiVersion = 'v16.0'; 1315 | const partnerAgent = 'gtmss-1.0.0-0.0.9'; 1316 | 1317 | const routeParams = 'events?access_token=' + testConfigurationData.apiAccessToken; 1318 | const requestEndpoint = [apiEndpoint, 1319 | apiVersion, 1320 | testConfigurationData.pixelId, 1321 | routeParams].join('/'); 1322 | 1323 | let requestData = { 1324 | data: [expectedEventData], 1325 | partner_agent: partnerAgent, 1326 | test_event_code: testData.test_event_code 1327 | }; 1328 | 1329 | const requestHeaderOptions = {headers: {'content-type': 'application/json'}, method: 'POST'}; 1330 | 1331 | let actualSuccessCallback, httpBody; 1332 | mock('sendHttpRequest', (postUrl, response, options, body) => { 1333 | actualSuccessCallback = response; 1334 | httpBody = body; 1335 | actualSuccessCallback(200, {}, ''); 1336 | }); 1337 | 1338 | 1339 | ___NOTES___ 1340 | 1341 | Created on 8/5/2020, 10:20:28 AM 1342 | --------------------------------------------------------------------------------