├── .editorconfig ├── .gitignore ├── .vscode ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── projects ├── angular-google-tag-manager │ ├── README.md │ ├── karma.conf.js │ ├── ng-package.json │ ├── package.json │ ├── src │ │ ├── lib │ │ │ ├── angular-google-tag-manager-config.service.spec.ts │ │ │ ├── angular-google-tag-manager-config.service.ts │ │ │ ├── angular-google-tag-manager.module.spec.ts │ │ │ ├── angular-google-tag-manager.module.ts │ │ │ ├── angular-google-tag-manager.providers.ts │ │ │ ├── angular-google-tag-manager.service.spec.ts │ │ │ ├── angular-google-tag-manager.service.ts │ │ │ ├── google-tag-manager-config.ts │ │ │ └── typings.d.ts │ │ ├── public-api.ts │ │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ └── tslint.json └── demo-application │ ├── src │ ├── app.config.ts │ ├── app │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.ts │ │ ├── page1 │ │ │ ├── page1.component.html │ │ │ └── page1.component.ts │ │ └── page2 │ │ │ ├── page2.component.html │ │ │ └── page2.component.ts │ ├── assets │ │ └── .gitkeep │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ └── styles.scss │ ├── tsconfig.app.json │ └── tslint.json ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | quote_type = single 11 | 12 | [*.md] 13 | max_line_length = off 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | /projects/*/node_modules 13 | 14 | # profiling files 15 | chrome-profiler-events.json 16 | speed-measure-plugin.json 17 | 18 | # IDEs and editors 19 | /.idea 20 | .project 21 | .classpath 22 | .c9/ 23 | *.launch 24 | .settings/ 25 | *.sublime-workspace 26 | 27 | # IDE - VSCode 28 | .vscode/* 29 | !.vscode/settings.json 30 | !.vscode/tasks.json 31 | !.vscode/launch.json 32 | !.vscode/extensions.json 33 | .history/* 34 | 35 | # misc 36 | /.angular/cache 37 | /.sass-cache 38 | /connect.lock 39 | /coverage 40 | /libpeerconnection.log 41 | npm-debug.log 42 | yarn-error.log 43 | testem.log 44 | /typings 45 | 46 | # System Files 47 | .DS_Store 48 | Thumbs.db 49 | .prettierrc 50 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "build", 7 | "problemMatcher": ["$tsc"], 8 | "group": { "kind": "build", "isDefault": true }, 9 | "label": "npm: build", 10 | "detail": "ng build" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [1.11.0] - 2024-12-04 8 | - Update to Angular 19 9 | - migrate to the new build system 10 | - Moved the demo app to fully standalone 11 | - Add a provider method in the lib 12 | - Removed protractor 13 | - Fix a test with expectation never reached 14 | - upgrade @types/jasmine to fit jasmine-core version 15 | 16 | ## [1.10.0] - 2024-08-11 17 | ### Added 18 | - Angular 18 support 19 | 20 | ## [1.9.0] - 2023-11-30 21 | ### Added 22 | - Angular 17 support 23 | 24 | ## [1.8.0] - 2023-05-23 25 | ### Added 26 | - Angular 16 support 27 | 28 | ## [1.7.1] - 2023-02-20 29 | 30 | ### Added 31 | - Silent mode 32 | ### Changed 33 | - Lib bump 34 | 35 | ## [1.7.0] - 2022-11-09 36 | 37 | ### Added 38 | - Angular 15 support 39 | ### Changed 40 | - Update to latest Angular 14 packages 41 | - Update all packages to latest version except: typescript @types/node 42 | - Update the lib to only support Angular ^15 43 | - Update the demo app to Angular 15 44 | - browserlistrc was removed 45 | - environment files have been deleted 46 | - angular.json was update to reflect that 47 | - Explicit tslib version in the lib 48 | - Removed demo app angular-google-tag-manager from the installed packages, it now use the one built locally 49 | - Project structure to only have one package.json 50 | ### Removed 51 | 52 | ## [1.6.1] - 2022-07-24 53 | 54 | ### Added 55 | ### Changed 56 | - Fix on ReferenceError 57 | ### Removed 58 | 59 | ## [1.6.0] - 2022-06-25 60 | 61 | ### Added 62 | - Angular 14 support 63 | ### Changed 64 | ### Removed 65 | 66 | 67 | ## [1.5.1] - 2022-06-02 68 | 69 | ### Added 70 | - use APP_INITIALIZER token to set config dynamically: https://github.com/mzuccaroli/angular-google-tag-manager/issues/135 71 | ### Changed 72 | - Bump dependencies 73 | 74 | ## [1.5.0] - 2022-01-03 75 | 76 | ### Added 77 | ### Changed 78 | - Updated all dependencies to latest version, or close to latest 79 | - Replaced tilde versioning with hat versioning (Tilde versioning of Angular is too strict #108) 80 | - Exceptions to the above: zone.js still in version 0.x and typescript is a special case for the Angular compiler 81 | - Removed tslint, as it no longer has any effect: executing npm run lint would state there was no linter installed 82 | - Removed tsickle and the closure compiler option, as they do not support any Typescript above 4.3 83 | - Updated build:prod script for Angular 13 standards 84 | ### Removed 85 | 86 | ## [1.4.4] - 2021-11-15 87 | 88 | ### Added 89 | ### Changed 90 | - package.json fix 91 | ### Removed 92 | 93 | ## [1.4.3] - 2021-11-13 94 | 95 | ### Added 96 | - Angular 13 support 97 | ### Changed 98 | ### Removed 99 | 100 | ## [1.4.2] - 2021-06-19 101 | 102 | ### Added 103 | - Optional CSP nonce 104 | ### Changed 105 | ### Removed 106 | 107 | ## [1.4.1] - 2021-06-05 108 | 109 | ### Added 110 | ### Changed 111 | - Typo fix 112 | ### Removed 113 | 114 | ## [1.4.0] - 2021-06-04 115 | 116 | ### Added 117 | - Angular 12 support 118 | ### Changed 119 | ### Removed 120 | 121 | ## [1.3.2] - 2021-04-30 122 | 123 | ### Added 124 | - config parameter for custom gtm.js resource path 125 | ### Changed 126 | ### Removed 127 | 128 | ## [1.3.1] - 2021-04-01 129 | 130 | ### Added 131 | ### Changed 132 | - Fix on first event push twice 133 | ### Removed 134 | 135 | ## [1.3.0] - 2021-02-07 136 | 137 | ### Added 138 | ### Changed 139 | - Angular 11 support 140 | ### Removed 141 | 142 | ## [1.2.4] - 2020‑12‑08 143 | 144 | ### Added 145 | ### Changed 146 | ### Removed 147 | - Remove noscript iframe 148 | ## [1.2.3] - 2020‑10‑27 149 | 150 | ### Added 151 | ### Changed 152 | - force use of https for google scripts 153 | ### Removed 154 | 155 | ## [1.2.2] - 2020‑09‑20 156 | 157 | ### Added 158 | ### Changed 159 | - support for Angular 10 strict mode 160 | ### Removed 161 | 162 | ## [1.2.1] - 2020‑09‑20 163 | 164 | ### Added 165 | - for root support 166 | ### Changed 167 | ### Removed 168 | 169 | ## [1.2.0] - 2020‑09‑20 170 | 171 | ### Added 172 | ### Changed 173 | - update to angular 10 174 | ### Removed 175 | 176 | ## [1.1.4] - 2020‑04‑23 177 | 178 | ### Added 179 | ### Changed 180 | - dependencies update 181 | ### Removed 182 | 183 | ## [1.1.3] - 2020‑04‑23 184 | 185 | ### Added 186 | ### Changed 187 | - readme update 188 | ### Removed 189 | 190 | ## [1.1.2] - 2020‑04‑22 191 | 192 | ### Added 193 | - gtm_auth support 194 | - gtm_preview support 195 | ### Changed 196 | ### Removed 197 | 198 | ## [1.1.1] - 2020‑01‑20 199 | 200 | ### Added 201 | ### Changed 202 | - readme update 203 | ### Removed 204 | 205 | ## [1.1.0] - 2020‑01‑20 206 | 207 | ### Added 208 | ### Changed 209 | - update requirements to angular 8 210 | ### Removed 211 | - browser-globals dependency 212 | 213 | ## [1.0.1] - 2020‑01‑14 214 | 215 | ### Added 216 | ### Changed 217 | - readme updated 218 | ### Removed 219 | 220 | ## [1.0.0] - 2020‑01‑14 221 | 222 | ### Added 223 | - demo application 224 | ### Changed 225 | ### Removed 226 | 227 | ## [0.0.2] - 2020‑01‑13 228 | 229 | ### Added 230 | ### Changed 231 | - public repo tag 232 | ### Removed 233 | 234 | ## [0.0.1] - 2020‑01‑13 235 | 236 | ### Added 237 | - changelog file 238 | ### Changed 239 | - performed npm audit fix 240 | - readme update 241 | ### Removed 242 | 243 | ## [0.0.0] - 2019‑05‑17 244 | 245 | ### Added 246 | - First implementation 247 | ### Changed 248 | ### Removed 249 | 250 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019-2021 Marco Zuccaroli. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Google Tag Manager Service 2 | 3 | A service library for integrate google tag manager in your angular project 4 | This library was generated with [Angular CLI](https://github.com/angular/angular-cli) 5 | For more info see this [how to install google tag manager article](https://itnext.io/how-to-add-google-tag-manager-to-an-angular-application-fc68624386e2) 6 | 7 | ## Getting Started 8 | 9 | After installing it you need to provide your GTM id in app.module.ts 10 | 11 | ``` 12 | providers: [ 13 | ... 14 | {provide: 'googleTagManagerId', useValue: YOUR_GTM_ID} 15 | ], 16 | ``` 17 | 18 | Or use the module's `forRoot` method 19 | 20 | ``` 21 | import { GoogleTagManagerModule } from 'angular-google-tag-manager'; 22 | 23 | imports: [ 24 | ... 25 | GoogleTagManagerModule.forRoot({ 26 | id: YOUR_GTM_ID, 27 | }) 28 | ] 29 | ``` 30 | 31 | Or use the standalone based provider 32 | 33 | ``` 34 | import { provideGoogleTagManager } from 'angular-google-tag-manager'; 35 | 36 | export const appConfig: ApplicationConfig = { 37 | providers: [ 38 | ... 39 | provideGoogleTagManager({ 40 | id: 'YOUR_GTM_ID', 41 | gtm_csp_none: 'CSP-NONCE', 42 | // gtm_auth: YOUR_GTM_AUTH, 43 | // gtm_preview: YOUR_GTM_RESOURCE_PATH 44 | // gtm_resource_path: 45 | // gtm_mode: "silent" | "noisy" 46 | }), 47 | ], 48 | }; 49 | ``` 50 | 51 | Or use the `APP_INITIALIZER` 52 | 53 | ``` 54 | import { GoogleTagManagerConfiguration } from 'angular-google-tag-manager-config.service'; 55 | 56 | imports: [ 57 | ... 58 | GoogleTagManagerModule.forRoot() 59 | ] 60 | 61 | providers: [ 62 | { 63 | ... 64 | provide: APP_INITIALIZER, 65 | useFactory: configInitializer, 66 | deps: [ 67 | HttpBackend, 68 | GoogleTagManagerConfiguration, 69 | ], 70 | multi: true, 71 | }, 72 | ], 73 | ``` 74 | 75 | set the config in the method assigned to useFactory 76 | 77 | ``` 78 | googleTagManagerConfiguration.set(googleTagManagerConfiguration); 79 | ``` 80 | 81 | inject the gtmService in your controller 82 | 83 | ``` 84 | constructor( 85 | ... 86 | private gtmService: GoogleTagManagerService, 87 | ) { } 88 | ``` 89 | 90 | then you can start pushing events on your gtm 91 | 92 | ``` 93 | this.router.events.forEach(item => { 94 | if (item instanceof NavigationEnd) { 95 | const gtmTag = { 96 | event: 'page', 97 | pageName: item.url 98 | }; 99 | 100 | this.gtmService.pushTag(gtmTag); 101 | } 102 | }); 103 | ``` 104 | 105 | if you want to recive tags without pushing events simply call the function to enable it 106 | 107 | ``` 108 | this.gtmService.addGtmToDom(); 109 | ``` 110 | 111 | ### Installing 112 | 113 | In your Angular project run 114 | 115 | ``` 116 | npm i --save angular-google-tag-manager 117 | ``` 118 | 119 | ### Custom configuration and GTM environments 120 | 121 | You can pass _gtm_preview_ and _gtm_auth_ optional variables to your GTM by providing them in app.module.ts 122 | 123 | ``` 124 | providers: [ 125 | ... 126 | {provide: 'googleTagManagerId', useValue: YOUR_GTM_ID}, 127 | {provide: 'googleTagManagerAuth', useValue: YOUR_GTM_AUTH}, 128 | {provide: 'googleTagManagerPreview', useValue: YOUR_GTM_ENV}, 129 | {provide: 'googleTagManagerResourcePath', useValue: YOUR_GTM_RESOURCE_PATH}, 130 | {provide: 'googleTagManagerCSPNonce', useValue: YOUR_CSP_NONCE}, 131 | {provide: 'googleTagManagerMode', useValue: "silent" | "noisy"} 132 | ], 133 | ``` 134 | 135 | Or using `forRoot` 136 | 137 | ``` 138 | import { GoogleTagManagerModule } from 'angular-google-tag-manager'; 139 | 140 | imports: [ 141 | ... 142 | GoogleTagManagerModule.forRoot({ 143 | id: YOUR_GTM_ID, 144 | gtm_auth: YOUR_GTM_AUTH, 145 | gtm_preview: YOUR_GTM_ENV 146 | }) 147 | ] 148 | ``` 149 | 150 | ## Authors 151 | 152 | - **Marco Zuccaroli** - _Initial work_ - [Marco Zuccaroli](https://github.com/mzuccaroli) 153 | 154 | See also the list of [contributors](https://github.com/mzuccaroli/angular-google-tag-manager/graphs/contributors) who participated in this project. 155 | 156 | ## License 157 | 158 | This project is licensed under the MIT License 159 | 160 | ## Acknowledgments 161 | 162 | - Thanks to PurpleBooth for the [Readme Template](https://gist.github.com/PurpleBooth/109311bb0361f32d87a2) 163 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-google-tag-manager": { 7 | "projectType": "library", 8 | "root": "projects/angular-google-tag-manager", 9 | "sourceRoot": "projects/angular-google-tag-manager/src", 10 | "prefix": "lib", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-angular:ng-packagr", 14 | "options": { 15 | "tsConfig": "projects/angular-google-tag-manager/tsconfig.lib.json", 16 | "project": "projects/angular-google-tag-manager/ng-package.json" 17 | }, 18 | "configurations": { 19 | "production": { 20 | "tsConfig": "projects/angular-google-tag-manager/tsconfig.lib.prod.json" 21 | } 22 | } 23 | }, 24 | "test": { 25 | "builder": "@angular-devkit/build-angular:karma", 26 | "options": { 27 | "main": "projects/angular-google-tag-manager/src/test.ts", 28 | "tsConfig": "projects/angular-google-tag-manager/tsconfig.spec.json", 29 | "karmaConfig": "projects/angular-google-tag-manager/karma.conf.js" 30 | } 31 | } 32 | } 33 | }, 34 | "demo-application": { 35 | "root": "projects/demo-application", 36 | "sourceRoot": "projects/demo-application/src", 37 | "projectType": "application", 38 | "prefix": "app", 39 | "schematics": { 40 | "@schematics/angular:component": { 41 | "style": "scss" 42 | } 43 | }, 44 | "architect": { 45 | "build": { 46 | "builder": "@angular-devkit/build-angular:application", 47 | "options": { 48 | "outputPath": { 49 | "base": "dist/demo-application" 50 | }, 51 | "index": "projects/demo-application/src/index.html", 52 | "polyfills": [ 53 | "projects/demo-application/src/polyfills.ts" 54 | ], 55 | "tsConfig": "projects/demo-application/tsconfig.app.json", 56 | "assets": [ 57 | "projects/demo-application/src/favicon.ico", 58 | "projects/demo-application/src/assets" 59 | ], 60 | "styles": [ 61 | "projects/demo-application/src/styles.scss" 62 | ], 63 | "scripts": [], 64 | "extractLicenses": false, 65 | "sourceMap": true, 66 | "optimization": false, 67 | "namedChunks": true, 68 | "browser": "projects/demo-application/src/main.ts" 69 | }, 70 | "configurations": { 71 | "production": { 72 | "optimization": true, 73 | "outputHashing": "all", 74 | "sourceMap": false, 75 | "namedChunks": false, 76 | "extractLicenses": true, 77 | "budgets": [ 78 | { 79 | "type": "initial", 80 | "maximumWarning": "2mb", 81 | "maximumError": "5mb" 82 | }, 83 | { 84 | "type": "anyComponentStyle", 85 | "maximumWarning": "6kb" 86 | } 87 | ] 88 | } 89 | }, 90 | "defaultConfiguration": "" 91 | }, 92 | "serve": { 93 | "builder": "@angular-devkit/build-angular:dev-server", 94 | "options": { 95 | "buildTarget": "demo-application:build" 96 | }, 97 | "configurations": { 98 | "production": { 99 | "buildTarget": "demo-application:build:production" 100 | } 101 | } 102 | }, 103 | "lint": { 104 | "builder": "@angular-devkit/build-angular:tslint", 105 | "options": { 106 | "tsConfig": [ 107 | "projects/demo-application/tsconfig.app.json" 108 | ], 109 | "exclude": [ 110 | "**/node_modules/**" 111 | ] 112 | } 113 | } 114 | } 115 | } 116 | }, 117 | "cli": { 118 | "analytics": false 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-google-tag-manager", 3 | "version": "1.11.0", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/mzuccaroli/angular-google-tag-manager" 8 | }, 9 | "description": "A service library for integrate google tag manager in your angular project", 10 | "author": "marcozuccaroli@gmail.com", 11 | "contributors": [ 12 | "Marco Zuccaroli ", 13 | "Nils Thomann ", 14 | "Alvaro Junqueira ", 15 | "Jason Gravell ", 16 | "Michael Doyle ", 17 | "Mathieu Maes ", 18 | "Stefano Cestari ", 19 | "LE LAY Olivier ", 20 | "Antoine Bursaux ", 21 | "Brandon Largeau ", 22 | "Perry M ", 23 | "Guillaume Masson ", 24 | "henriqueformiga " 25 | ], 26 | "scripts": { 27 | "ng": "ng", 28 | "start": "ng serve", 29 | "build:demo-application": "ng build demo-application", 30 | "build:lib": "ng build angular-google-tag-manager", 31 | "build:lib:prod": "ng build angular-google-tag-manager --configuration=production", 32 | "test:lib": "ng test angular-google-tag-manager", 33 | "lint:lib": "ng lint angular-google-tag-manager", 34 | "version": "ng version" 35 | }, 36 | "private": true, 37 | "dependencies": { 38 | "@angular/animations": "~19.0.0", 39 | "@angular/common": "~19.0.0", 40 | "@angular/compiler": "~19.0.0", 41 | "@angular/core": "~19.0.0", 42 | "@angular/forms": "~19.0.0", 43 | "@angular/platform-browser": "~19.0.0", 44 | "@angular/platform-browser-dynamic": "~19.0.0", 45 | "@angular/router": "~19.0.0", 46 | "rxjs": "~7.8.1", 47 | "tslib": "~2.5.2", 48 | "zone.js": "~0.15.0" 49 | }, 50 | "devDependencies": { 51 | "@angular-devkit/build-angular": "~19.0.0", 52 | "@angular/cli": "~19.0.0", 53 | "@angular/compiler-cli": "~19.0.0", 54 | "@types/jasmine": "~5.1.4", 55 | "@types/jasminewd2": "~2.0.10", 56 | "@types/node": "~18.15.3", 57 | "codelyzer": "~6.0.2", 58 | "jasmine-core": "~5.1.1", 59 | "jasmine-spec-reporter": "~7.0.0", 60 | "karma": "~6.4.2", 61 | "karma-chrome-launcher": "~3.2.0", 62 | "karma-coverage-istanbul-reporter": "~3.0.3", 63 | "karma-jasmine": "~5.1.0", 64 | "karma-jasmine-html-reporter": "~2.1.0", 65 | "ng-packagr": "~19.0.0", 66 | "ts-node": "~10.9.1", 67 | "typescript": "~5.5.4" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /projects/angular-google-tag-manager/README.md: -------------------------------------------------------------------------------- 1 | # Angular Google Tag Manager Service 2 | 3 | A service library for integrate google tag manager in your angular project 4 | This library was generated with [Angular CLI](https://github.com/angular/angular-cli) 5 | For more info see this [how to install google tag manager article](https://itnext.io/how-to-add-google-tag-manager-to-an-angular-application-fc68624386e2) 6 | 7 | ## Getting Started 8 | 9 | After installing it you need to provide your GTM id in app.module.ts 10 | 11 | ``` 12 | providers: [ 13 | ... 14 | {provide: 'googleTagManagerId', useValue: YOUR_GTM_ID} 15 | ], 16 | ``` 17 | 18 | Or use the module's `forRoot` method 19 | 20 | ``` 21 | import { GoogleTagManagerModule } from 'angular-google-tag-manager'; 22 | 23 | imports: [ 24 | ... 25 | GoogleTagManagerModule.forRoot({ 26 | id: YOUR_GTM_ID, 27 | }) 28 | ] 29 | ``` 30 | 31 | Or use the `APP_INITIALIZER` 32 | 33 | ``` 34 | import { GoogleTagManagerConfiguration } from 'angular-google-tag-manager-config.service'; 35 | 36 | imports: [ 37 | ... 38 | GoogleTagManagerModule.forRoot() 39 | ] 40 | 41 | providers: [ 42 | { 43 | ... 44 | provide: APP_INITIALIZER, 45 | useFactory: configInitializer, 46 | deps: [ 47 | HttpBackend, 48 | GoogleTagManagerConfiguration, 49 | ], 50 | multi: true, 51 | }, 52 | ], 53 | ``` 54 | 55 | set the config in the method assigned to useFactory 56 | 57 | ``` 58 | googleTagManagerConfiguration.set(googleTagManagerConfiguration); 59 | ``` 60 | 61 | inject the gtmService in your controller 62 | 63 | ``` 64 | constructor( 65 | ... 66 | private gtmService: GoogleTagManagerService, 67 | ) { } 68 | ``` 69 | 70 | then you can start pushing events on your gtm 71 | 72 | ``` 73 | this.router.events.forEach(item => { 74 | if (item instanceof NavigationEnd) { 75 | const gtmTag = { 76 | event: 'page', 77 | pageName: item.url 78 | }; 79 | 80 | this.gtmService.pushTag(gtmTag); 81 | } 82 | }); 83 | ``` 84 | 85 | if you want to recive tags without pushing events simply call the function to enable it 86 | 87 | ``` 88 | this.gtmService.addGtmToDom(); 89 | ``` 90 | 91 | ### Installing 92 | 93 | In your Angular project run 94 | 95 | ``` 96 | npm i --save angular-google-tag-manager 97 | ``` 98 | 99 | ### Custom configuration and GTM environments 100 | 101 | You can pass _gtm_preview_ and _gtm_auth_ optional variables to your GTM by providing them in app.module.ts 102 | 103 | In case you'll need to fetch your gtm.js resource trough a first party proxy ( so to bypass any ad-blocker interference in on the client browser ) you can use the _gtm_resource_path_ parameter. 104 | In that case the resource will be fetched from the specified path instead that from 'https://www.googletagmanager.com/gtm.js' (all of the other queryparams will be mantained) 105 | 106 | ``` 107 | providers: [ 108 | ... 109 | {provide: 'googleTagManagerId', useValue: YOUR_GTM_ID}, 110 | {provide: 'googleTagManagerAuth', useValue: YOUR_GTM_AUTH}, 111 | {provide: 'googleTagManagerPreview', useValue: YOUR_GTM_ENV}, 112 | {provide: 'googleTagManagerResourcePath', useValue: YOUR_GTM_RESOURCE_PATH} 113 | ], 114 | ``` 115 | 116 | Or using `forRoot` 117 | 118 | ``` 119 | import { GoogleTagManagerModule } from 'angular-google-tag-manager'; 120 | 121 | imports: [ 122 | ... 123 | GoogleTagManagerModule.forRoot({ 124 | id: YOUR_GTM_ID, 125 | gtm_auth: YOUR_GTM_AUTH, 126 | gtm_preview: YOUR_GTM_ENV, 127 | gtm_resource_path: YOUR_GTM_RESOURCE_PATH 128 | }) 129 | ] 130 | ``` 131 | 132 | ## Authors 133 | 134 | - **Marco Zuccaroli** - _Initial work_ - [Marco Zuccaroli](https://github.com/mzuccaroli) 135 | 136 | See also the list of [contributors](https://github.com/mzuccaroli/angular-google-tag-manager/graphs/contributors) who participated in this project. 137 | 138 | ## License 139 | 140 | This project is licensed under the MIT License 141 | 142 | ## Acknowledgments 143 | 144 | - Thanks to PurpleBooth for the [Readme Template](https://gist.github.com/PurpleBooth/109311bb0361f32d87a2) 145 | -------------------------------------------------------------------------------- /projects/angular-google-tag-manager/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/angular-google-tag-manager'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /projects/angular-google-tag-manager/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/angular-google-tag-manager", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/angular-google-tag-manager/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-google-tag-manager", 3 | "version": "1.11.0", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/mzuccaroli/angular-google-tag-manager" 8 | }, 9 | "description": "A service library for integrate google tag manager in your angular project", 10 | "author": "marcozuccaroli@gmail.com", 11 | "contributors": [ 12 | "Marco Zuccaroli ", 13 | "Nils Thomann ", 14 | "Alvaro Junqueira ", 15 | "Jason Gravell ", 16 | "Michael Doyle ", 17 | "Mathieu Maes ", 18 | "Stefano Cestari ", 19 | "LE LAY Olivier ", 20 | "Antoine Bursaux ", 21 | "Brandon Largeau ", 22 | "Perry M ", 23 | "Guillaume Masson ", 24 | "henriqueformiga " 25 | ], 26 | "peerDependencies": { 27 | "@angular/common": "^19.0.0", 28 | "@angular/compiler": "^19.0.0" 29 | }, 30 | "dependencies": { 31 | "tslib": "^2.5.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /projects/angular-google-tag-manager/src/lib/angular-google-tag-manager-config.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, TestBed } from '@angular/core/testing'; 2 | import { GoogleTagManagerModule } from './angular-google-tag-manager.module'; 3 | import { 4 | GoogleTagManagerConfigService, 5 | GoogleTagManagerConfiguration, 6 | } from './angular-google-tag-manager-config.service'; 7 | 8 | describe('GoogleTagManagerConfigService', () => { 9 | describe('can set config with id only', () => { 10 | beforeEach(() => { 11 | TestBed.configureTestingModule({ 12 | imports: [ 13 | GoogleTagManagerModule.forRoot({ 14 | id: 'TEST_GTM_ID', 15 | }), 16 | ], 17 | providers: [ 18 | { 19 | provider: GoogleTagManagerConfigService, 20 | useClass: GoogleTagManagerConfiguration, 21 | }, 22 | ], 23 | }); 24 | const googleTagManagerConfigService = 25 | TestBed.inject( 26 | GoogleTagManagerConfiguration 27 | ); 28 | googleTagManagerConfigService.set({ id: 'TEST_GTM_ID' }); 29 | }); 30 | 31 | it('returns config with expected id', inject( 32 | [GoogleTagManagerConfiguration], 33 | (service: GoogleTagManagerConfiguration) => { 34 | expect(service).toBeTruthy(); 35 | const config = service.get(); 36 | expect(config).toBeTruthy(); 37 | expect(config.id).toBe('TEST_GTM_ID'); 38 | expect(config.gtm_auth).toBeFalsy(); 39 | expect(config.gtm_preview).toBeFalsy(); 40 | } 41 | )); 42 | }); 43 | 44 | describe('with other options configured', () => { 45 | beforeEach(() => { 46 | TestBed.configureTestingModule({ 47 | imports: [ 48 | GoogleTagManagerModule.forRoot({ 49 | id: 'TEST_GTM_ID', 50 | gtm_auth: 'TEST_GTM_AUTH', 51 | gtm_preview: 'TEST_GTM_PREVIEW', 52 | }), 53 | ], 54 | providers: [ 55 | { 56 | provider: GoogleTagManagerConfigService, 57 | useClass: GoogleTagManagerConfiguration, 58 | }, 59 | ], 60 | }); 61 | const googleTagManagerConfigService = 62 | TestBed.inject( 63 | GoogleTagManagerConfiguration 64 | ); 65 | googleTagManagerConfigService.set({ 66 | id: 'TEST_GTM_ID', 67 | gtm_auth: 'TEST_GTM_AUTH', 68 | gtm_preview: 'TEST_GTM_PREVIEW', 69 | }); 70 | }); 71 | 72 | it('returns config with expected id, gtm_auth and gtm_preview', inject( 73 | [GoogleTagManagerConfiguration], 74 | (service: GoogleTagManagerConfiguration) => { 75 | expect(service).toBeTruthy(); 76 | const config = service.get(); 77 | expect(config).toBeTruthy(); 78 | expect(config.id).toBe('TEST_GTM_ID'); 79 | expect(config.gtm_auth).toBe('TEST_GTM_AUTH'); 80 | expect(config.gtm_preview).toBe('TEST_GTM_PREVIEW'); 81 | } 82 | )); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /projects/angular-google-tag-manager/src/lib/angular-google-tag-manager-config.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, InjectionToken, Optional } from '@angular/core'; 2 | import { GoogleTagManagerConfig } from './google-tag-manager-config'; 3 | 4 | export const GoogleTagManagerConfigService = 5 | new InjectionToken('google-tag-manager-config'); 6 | 7 | // adapted from https://github.com/auth0/auth0-angular#dynamic-configuration 8 | @Injectable({ providedIn: 'root' }) 9 | export class GoogleTagManagerConfiguration { 10 | private _googleTagManagerConfig: GoogleTagManagerConfig = { 11 | id: null, 12 | gtm_auth: '', 13 | gtm_preview: '', 14 | }; 15 | 16 | constructor( 17 | @Optional() 18 | @Inject(GoogleTagManagerConfigService) 19 | googleTagManagerConfig?: GoogleTagManagerConfig 20 | ) { 21 | if (googleTagManagerConfig) { 22 | this.set(googleTagManagerConfig); 23 | } 24 | } 25 | 26 | public set(googleTagManagerConfig: GoogleTagManagerConfig): void { 27 | this._googleTagManagerConfig = googleTagManagerConfig; 28 | } 29 | 30 | public get(): GoogleTagManagerConfig { 31 | return this._googleTagManagerConfig; 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /projects/angular-google-tag-manager/src/lib/angular-google-tag-manager.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, TestBed } from '@angular/core/testing'; 2 | 3 | import { GoogleTagManagerService } from './angular-google-tag-manager.service'; 4 | import { GoogleTagManagerModule } from './angular-google-tag-manager.module'; 5 | import { 6 | GoogleTagManagerConfigService, 7 | GoogleTagManagerConfiguration, 8 | } from './angular-google-tag-manager-config.service'; 9 | 10 | describe('GoogleTagManagerModule', () => { 11 | describe('with just id configured', () => { 12 | beforeEach(() => { 13 | TestBed.configureTestingModule({ 14 | imports: [ 15 | GoogleTagManagerModule.forRoot({ 16 | id: 'TEST_GTM_ID', 17 | }), 18 | ], 19 | }); 20 | }); 21 | 22 | it('should create a GoogleTagManagerService', inject( 23 | [GoogleTagManagerService], 24 | (service: GoogleTagManagerService) => { 25 | expect(service).toBeTruthy(); 26 | } 27 | )); 28 | }); 29 | 30 | describe('with other options configured', () => { 31 | beforeEach(() => { 32 | TestBed.configureTestingModule({ 33 | imports: [ 34 | GoogleTagManagerModule.forRoot({ 35 | id: 'TEST_GTM_ID', 36 | gtm_auth: 'TEST_GTM_AUTH', 37 | gtm_preview: 'TEST_GTM_PREVIEW', 38 | }), 39 | ], 40 | }); 41 | }); 42 | 43 | it('should create a GoogleTagManagerService', inject( 44 | [GoogleTagManagerService], 45 | (service: GoogleTagManagerService) => { 46 | expect(service).toBeTruthy(); 47 | } 48 | )); 49 | }); 50 | 51 | describe('with just id configured via GoogleTagManagerConfigService', () => { 52 | beforeEach(() => { 53 | TestBed.configureTestingModule({ 54 | imports: [GoogleTagManagerModule.forRoot()], 55 | providers: [ 56 | { 57 | provider: GoogleTagManagerConfigService, 58 | useClass: GoogleTagManagerConfiguration, 59 | }, 60 | ], 61 | }); 62 | const googleTagManagerConfigService = 63 | TestBed.inject( 64 | GoogleTagManagerConfiguration 65 | ); 66 | googleTagManagerConfigService.set({ id: 'TEST_GTM_ID' }); 67 | }); 68 | 69 | it('should create a GoogleTagManagerService', inject( 70 | [GoogleTagManagerService], 71 | (service: GoogleTagManagerService) => { 72 | expect(service).toBeTruthy(); 73 | } 74 | )); 75 | }); 76 | 77 | describe('with other options configured via GoogleTagManagerConfigService', () => { 78 | beforeEach(() => { 79 | TestBed.configureTestingModule({ 80 | imports: [GoogleTagManagerModule.forRoot()], 81 | providers: [ 82 | { 83 | provider: GoogleTagManagerConfigService, 84 | useClass: GoogleTagManagerConfiguration, 85 | }, 86 | ], 87 | }); 88 | const googleTagManagerConfigService = 89 | TestBed.inject( 90 | GoogleTagManagerConfiguration 91 | ); 92 | googleTagManagerConfigService.set({ 93 | id: 'TEST_GTM_ID', 94 | gtm_auth: 'TEST_GTM_AUTH', 95 | gtm_preview: 'TEST_GTM_PREVIEW', 96 | }); 97 | }); 98 | 99 | it('should create a GoogleTagManagerService by retrieving config from GoogleTagManagerConfigService ', inject( 100 | [GoogleTagManagerService], 101 | (service: GoogleTagManagerService) => { 102 | expect(service).toBeTruthy(); 103 | } 104 | )); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /projects/angular-google-tag-manager/src/lib/angular-google-tag-manager.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from '@angular/core'; 2 | import { GoogleTagManagerConfigService } from './angular-google-tag-manager-config.service'; 3 | import { GoogleTagManagerConfig } from './google-tag-manager-config'; 4 | 5 | @NgModule() 6 | export class GoogleTagManagerModule { 7 | public static forRoot( 8 | config?: GoogleTagManagerConfig 9 | ): ModuleWithProviders { 10 | return { 11 | ngModule: GoogleTagManagerModule, 12 | providers: [{ provide: GoogleTagManagerConfigService, useValue: config }], 13 | }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /projects/angular-google-tag-manager/src/lib/angular-google-tag-manager.providers.ts: -------------------------------------------------------------------------------- 1 | import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core'; 2 | import { GoogleTagManagerConfig } from './google-tag-manager-config'; 3 | 4 | export function provideGoogleTagManager( 5 | config: GoogleTagManagerConfig 6 | ): EnvironmentProviders { 7 | return makeEnvironmentProviders([ 8 | { provide: 'googleTagManagerId', useValue: config.id }, 9 | { provide: 'googleTagManagerAuth', useValue: config.gtm_auth }, 10 | { provide: 'googleTagManagerPreview', useValue: config.gtm_preview }, 11 | { 12 | provide: 'googleTagManagerResourcePath', 13 | useValue: config.gtm_resource_path, 14 | }, 15 | { provide: 'googleTagManagerCSPNonce', useValue: config.gtm_csp_none }, 16 | { provide: 'googleTagManagerMode', useValue: config.gtm_mode }, 17 | ]); 18 | } 19 | -------------------------------------------------------------------------------- /projects/angular-google-tag-manager/src/lib/angular-google-tag-manager.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { GoogleTagManagerService } from './angular-google-tag-manager.service'; 3 | 4 | describe('GoogleTagManagerService', () => { 5 | const testObject = { testKey: 'testValue' }; 6 | 7 | beforeEach( 8 | waitForAsync(() => { 9 | TestBed.configureTestingModule({ 10 | providers: [ 11 | GoogleTagManagerService, 12 | {provide: 'googleTagManagerId', useValue: 'TEST_GTM_ID'} 13 | ] 14 | }); 15 | window.dataLayer = []; 16 | }) 17 | ); 18 | 19 | it('should be created', 20 | inject([GoogleTagManagerService], (service: GoogleTagManagerService) => { 21 | expect(service).toBeTruthy(); 22 | })); 23 | 24 | it('should provide a dataLayer taken from the global window variable', 25 | inject([GoogleTagManagerService], (service: GoogleTagManagerService) => { 26 | expect(service.getDataLayer()).toEqual([]); 27 | window.dataLayer = [testObject]; 28 | expect(service.getDataLayer()).toEqual([testObject]); 29 | })); 30 | 31 | it('should init the GTM dataLayer on first push item', 32 | inject([GoogleTagManagerService], async (service: GoogleTagManagerService) => { 33 | await service.pushTag(testObject); 34 | expect(window.dataLayer[0].event).toEqual('gtm.js'); 35 | })); 36 | 37 | it('should be push objects in the dataLayer', 38 | inject([GoogleTagManagerService], async (service: GoogleTagManagerService) => { 39 | await service.pushTag(testObject); 40 | expect(window.dataLayer[1]).toEqual(testObject); 41 | expect(window.dataLayer[2]).toBeFalsy(); 42 | // we know it is loaded so no need to call .then on pushTag here 43 | await service.pushTag(testObject); 44 | expect(window.dataLayer[1]).toEqual(testObject); 45 | expect(window.dataLayer[2]).toEqual(testObject); 46 | expect(window.dataLayer[3]).toBeFalsy(); 47 | })); 48 | 49 | it('should be able to initialize the dom with an iframe and a script element', 50 | inject([GoogleTagManagerService], async (service: GoogleTagManagerService) => { 51 | await service.pushTag(testObject); 52 | // const iframe = document.querySelector('body > noscript > iframe'); 53 | // expect(iframe).toBeTruthy(); 54 | // expect(iframe.getAttribute('src')).toContain('https://www.googletagmanager.com/ns.html?id='); 55 | const script = document.querySelector('#GTMscript'); 56 | expect(script).toBeTruthy(); 57 | expect(script.getAttribute('src')).toContain('https://www.googletagmanager.com/gtm.js?id='); 58 | })); 59 | 60 | it('should be able to initialize the dataLayer with some defaults values of a page', 61 | inject([GoogleTagManagerService], async (service: GoogleTagManagerService) => { 62 | await service.pushTag(testObject); 63 | expect(window.dataLayer[0]['gtm.start']).toBeTruthy(); 64 | expect(window.dataLayer[0].event).toEqual('gtm.js'); 65 | })); 66 | }); 67 | -------------------------------------------------------------------------------- /projects/angular-google-tag-manager/src/lib/angular-google-tag-manager.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, Optional } from '@angular/core'; 2 | import { GoogleTagManagerConfiguration } from './angular-google-tag-manager-config.service'; 3 | import { GoogleTagManagerConfig } from './google-tag-manager-config'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class GoogleTagManagerService { 9 | private isLoaded = false; 10 | private config: GoogleTagManagerConfig | null; 11 | 12 | private browserGlobals = { 13 | windowRef(): any { 14 | return window; 15 | }, 16 | documentRef(): any { 17 | return document; 18 | }, 19 | }; 20 | 21 | constructor( 22 | @Optional() 23 | @Inject(GoogleTagManagerConfiguration) 24 | public googleTagManagerConfiguration: GoogleTagManagerConfiguration, 25 | @Optional() @Inject('googleTagManagerId') public googleTagManagerId: string, 26 | @Optional() @Inject('googleTagManagerMode') public googleTagManagerMode: "silent"|"noisy" = "noisy", 27 | @Optional() 28 | @Inject('googleTagManagerAuth') 29 | public googleTagManagerAuth: string, 30 | @Optional() 31 | @Inject('googleTagManagerPreview') 32 | public googleTagManagerPreview: string, 33 | @Optional() 34 | @Inject('googleTagManagerResourcePath') 35 | public googleTagManagerResourcePath: string, 36 | @Optional() 37 | @Inject('googleTagManagerCSPNonce') 38 | public googleTagManagerCSPNonce: string 39 | ) { 40 | this.config = this.googleTagManagerConfiguration?.get(); 41 | if (this.config == null) { 42 | this.config = { id: null }; 43 | } 44 | 45 | this.config = { 46 | ...this.config, 47 | id: googleTagManagerId || this.config.id, 48 | gtm_auth: googleTagManagerAuth || this.config.gtm_auth, 49 | gtm_preview: googleTagManagerPreview || this.config.gtm_preview, 50 | gtm_resource_path: 51 | googleTagManagerResourcePath || this.config.gtm_resource_path, 52 | }; 53 | if (this.config.id == null) { 54 | return; 55 | } 56 | } 57 | 58 | private checkForId(): boolean { 59 | if( this.googleTagManagerMode !== "silent" && !this.config.id ) { 60 | throw new Error('Google tag manager ID not provided.'); 61 | } else if( !this.config.id ) { 62 | return false; 63 | } 64 | return true; 65 | } 66 | 67 | public getDataLayer(): any[] { 68 | this.checkForId(); 69 | const window = this.browserGlobals.windowRef(); 70 | window.dataLayer = window.dataLayer || []; 71 | return window.dataLayer; 72 | } 73 | 74 | private pushOnDataLayer(obj: object): void { 75 | this.checkForId(); 76 | const dataLayer = this.getDataLayer(); 77 | dataLayer.push(obj); 78 | } 79 | 80 | public addGtmToDom(): Promise { 81 | return new Promise((resolve, reject) => { 82 | if (this.isLoaded) { 83 | return resolve(this.isLoaded); 84 | } else if( !this.checkForId() ) { 85 | return resolve(false); 86 | } 87 | const doc = this.browserGlobals.documentRef(); 88 | this.pushOnDataLayer({ 89 | 'gtm.start': new Date().getTime(), 90 | event: 'gtm.js', 91 | }); 92 | 93 | const gtmScript = doc.createElement('script'); 94 | gtmScript.id = 'GTMscript'; 95 | gtmScript.async = true; 96 | gtmScript.src = this.applyGtmQueryParams( 97 | this.config.gtm_resource_path 98 | ? this.config.gtm_resource_path 99 | : 'https://www.googletagmanager.com/gtm.js' 100 | ); 101 | gtmScript.addEventListener('load', () => { 102 | return resolve((this.isLoaded = true)); 103 | }); 104 | gtmScript.addEventListener('error', () => { 105 | return reject(false); 106 | }); 107 | if (this.googleTagManagerCSPNonce) { 108 | gtmScript.setAttribute('nonce', this.googleTagManagerCSPNonce); 109 | } 110 | doc.head.insertBefore(gtmScript, doc.head.firstChild); 111 | }); 112 | } 113 | 114 | public pushTag(item: object): Promise { 115 | return new Promise((resolve, reject) => { 116 | if( !this.checkForId() ) { 117 | return resolve(); 118 | } 119 | 120 | if (!this.isLoaded) { 121 | this.addGtmToDom() 122 | .then(() => { 123 | this.pushOnDataLayer(item); 124 | return resolve(); 125 | }) 126 | .catch(() => reject()); 127 | } else { 128 | this.pushOnDataLayer(item); 129 | return resolve(); 130 | } 131 | }); 132 | } 133 | 134 | private applyGtmQueryParams(url: string): string { 135 | if (url.indexOf('?') === -1) { 136 | url += '?'; 137 | } 138 | 139 | return ( 140 | url + 141 | Object.keys(this.config) 142 | .filter((k) => this.config[k]) 143 | .map((k) => `${k}=${this.config[k]}`) 144 | .join('&') 145 | ); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /projects/angular-google-tag-manager/src/lib/google-tag-manager-config.ts: -------------------------------------------------------------------------------- 1 | export interface GoogleTagManagerConfig { 2 | id: string | null; 3 | gtm_auth?: string; 4 | gtm_preview?: string; 5 | [key: string]: string | null | undefined; 6 | } 7 | -------------------------------------------------------------------------------- /projects/angular-google-tag-manager/src/lib/typings.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | interface Window { 5 | dataLayer: any[] | null; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/angular-google-tag-manager/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of angular-google-tag-manager 3 | */ 4 | 5 | export * from './lib/angular-google-tag-manager-config.service'; 6 | export * from './lib/angular-google-tag-manager.module'; 7 | export * from './lib/angular-google-tag-manager.providers'; 8 | export * from './lib/angular-google-tag-manager.service'; 9 | export * from './lib/google-tag-manager-config'; 10 | -------------------------------------------------------------------------------- /projects/angular-google-tag-manager/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | import 'zone.js'; 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting, 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { 12 | errorOnUnknownElements: true, 13 | errorOnUnknownProperties: true 14 | }); 15 | -------------------------------------------------------------------------------- /projects/angular-google-tag-manager/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "strictNullChecks": false, 10 | "types": [] 11 | }, 12 | "exclude": ["src/test.ts", "**/*.spec.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /projects/angular-google-tag-manager/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /projects/angular-google-tag-manager/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": ["src/test.ts"], 12 | "include": [ 13 | "**/*.spec.ts", 14 | "**/*.d.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /projects/angular-google-tag-manager/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "lib", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "lib", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/demo-application/src/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig } from '@angular/core'; 2 | import { Routes, provideRouter } from '@angular/router'; 3 | import { provideGoogleTagManager } from 'projects/angular-google-tag-manager/src/lib/angular-google-tag-manager.providers'; 4 | import { Page1Component } from './app/page1/page1.component'; 5 | import { Page2Component } from './app/page2/page2.component'; 6 | 7 | const routes: Routes = [ 8 | { 9 | path: 'page1', 10 | component: Page1Component, 11 | }, 12 | { 13 | path: 'page2', 14 | component: Page2Component, 15 | }, 16 | ]; 17 | export const appConfig: ApplicationConfig = { 18 | providers: [ 19 | provideRouter(routes), 20 | provideGoogleTagManager({ 21 | id: 'GTM-PV8586C', 22 | gtm_csp_none: 'CSP-NONCE', 23 | // gtm_auth: YOUR_GTM_AUTH, 24 | // gtm_preview: YOUR_GTM_ENV 25 | }), 26 | ], 27 | }; 28 | -------------------------------------------------------------------------------- /projects/demo-application/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | Welcome to {{ title }}! 5 |

6 | Angular Logo 8 |
9 |

This is a demo application for the angular angular-google-tag-manager package

10 | click on the pages link anevent then inspect this page and take a look to the google tag manager included scripts and 11 | "dataLayer" global variable 12 |

13 | page 1 - 14 | page 2 - 15 | 16 |

17 | -------------------------------------------------------------------------------- /projects/demo-application/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzuccaroli/angular-google-tag-manager/91a9e0f8f145f426e060a77c6da969030b5a8f66/projects/demo-application/src/app/app.component.scss -------------------------------------------------------------------------------- /projects/demo-application/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { 3 | NavigationEnd, 4 | Router, 5 | RouterLink, 6 | RouterOutlet, 7 | } from '@angular/router'; 8 | import { GoogleTagManagerService } from 'angular-google-tag-manager'; 9 | 10 | @Component({ 11 | selector: 'app-root', 12 | templateUrl: './app.component.html', 13 | styleUrls: ['./app.component.scss'], 14 | imports: [RouterOutlet, RouterLink], 15 | }) 16 | export class AppComponent implements OnInit { 17 | constructor( 18 | private router: Router, 19 | private gtmService: GoogleTagManagerService 20 | ) {} 21 | 22 | title = 'angular-google-tag-manager demo application'; 23 | 24 | customEvent() { 25 | // push GTM data layer with a custom event 26 | const gtmTag = { 27 | event: 'button-click', 28 | data: 'my-custom-event', 29 | }; 30 | this.gtmService.pushTag(gtmTag); 31 | 32 | alert('this is a custom event'); 33 | } 34 | 35 | ngOnInit() { 36 | // push GTM data layer for every visited page 37 | this.router.events.forEach((item) => { 38 | if (item instanceof NavigationEnd) { 39 | const gtmTag = { 40 | event: 'page', 41 | pageName: item.url, 42 | }; 43 | 44 | this.gtmService.pushTag(gtmTag); 45 | } 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /projects/demo-application/src/app/page1/page1.component.html: -------------------------------------------------------------------------------- 1 |

2 | this is page1 3 |

4 | -------------------------------------------------------------------------------- /projects/demo-application/src/app/page1/page1.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-page1', 5 | templateUrl: './page1.component.html', 6 | }) 7 | export class Page1Component {} 8 | -------------------------------------------------------------------------------- /projects/demo-application/src/app/page2/page2.component.html: -------------------------------------------------------------------------------- 1 |

2 | this is page2 3 |

4 | -------------------------------------------------------------------------------- /projects/demo-application/src/app/page2/page2.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-page2', 5 | templateUrl: './page2.component.html', 6 | }) 7 | export class Page2Component {} 8 | -------------------------------------------------------------------------------- /projects/demo-application/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzuccaroli/angular-google-tag-manager/91a9e0f8f145f426e060a77c6da969030b5a8f66/projects/demo-application/src/assets/.gitkeep -------------------------------------------------------------------------------- /projects/demo-application/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzuccaroli/angular-google-tag-manager/91a9e0f8f145f426e060a77c6da969030b5a8f66/projects/demo-application/src/favicon.ico -------------------------------------------------------------------------------- /projects/demo-application/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DemoApplication 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /projects/demo-application/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from 'projects/demo-application/src/app.config'; 3 | import { AppComponent } from 'projects/demo-application/src/app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig).catch((err) => 6 | console.error(err) 7 | ); 8 | -------------------------------------------------------------------------------- /projects/demo-application/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /projects/demo-application/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /projects/demo-application/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": ["src/main.ts", "src/polyfills.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /projects/demo-application/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "align": { 8 | "options": [ 9 | "parameters", 10 | "statements" 11 | ] 12 | }, 13 | "array-type": false, 14 | "arrow-parens": false, 15 | "arrow-return-shorthand": true, 16 | "deprecation": { 17 | "severity": "warn" 18 | }, 19 | "curly": true, 20 | "import-blacklist": [ 21 | true, 22 | "rxjs/Rx" 23 | ], 24 | "interface-name": false, 25 | "eofline": true, 26 | "max-classes-per-file": false, 27 | "import-spacing": true, 28 | "indent": { 29 | "options": [ 30 | "spaces" 31 | ] 32 | }, 33 | "max-line-length": [ 34 | true, 35 | 140 36 | ], 37 | "member-access": false, 38 | "member-ordering": [ 39 | true, 40 | { 41 | "order": [ 42 | "static-field", 43 | "instance-field", 44 | "static-method", 45 | "instance-method" 46 | ] 47 | } 48 | ], 49 | "no-any": true, 50 | "no-consecutive-blank-lines": false, 51 | "no-console": [ 52 | true, 53 | "debug", 54 | "info", 55 | "time", 56 | "timeEnd", 57 | "trace" 58 | ], 59 | "no-empty": false, 60 | "no-inferrable-types": [ 61 | true, 62 | "ignore-params" 63 | ], 64 | "no-non-null-assertion": true, 65 | "no-redundant-jsdoc": true, 66 | "no-switch-case-fall-through": true, 67 | "no-var-requires": false, 68 | "object-literal-key-quotes": [ 69 | true, 70 | "as-needed" 71 | ], 72 | "object-literal-sort-keys": false, 73 | "ordered-imports": false, 74 | "quotemark": [ 75 | true, 76 | "single" 77 | ], 78 | "trailing-comma": false, 79 | "no-output-on-prefix": true, 80 | "no-inputs-metadata-property": true, 81 | "no-outputs-metadata-property": true, 82 | "no-host-metadata-property": true, 83 | "no-input-rename": true, 84 | "no-output-rename": true, 85 | "use-lifecycle-interface": true, 86 | "semicolon": { 87 | "options": [ 88 | "always" 89 | ] 90 | }, 91 | "space-before-function-paren": { 92 | "options": { 93 | "anonymous": "never", 94 | "asyncArrow": "always", 95 | "constructor": "never", 96 | "method": "never", 97 | "named": "never" 98 | } 99 | }, 100 | "use-pipe-transform-interface": true, 101 | "typedef-whitespace": { 102 | "options": [ 103 | { 104 | "call-signature": "nospace", 105 | "index-signature": "nospace", 106 | "parameter": "nospace", 107 | "property-declaration": "nospace", 108 | "variable-declaration": "nospace" 109 | }, 110 | { 111 | "call-signature": "onespace", 112 | "index-signature": "onespace", 113 | "parameter": "onespace", 114 | "property-declaration": "onespace", 115 | "variable-declaration": "onespace" 116 | } 117 | ] 118 | }, 119 | "component-class-suffix": true, 120 | "directive-class-suffix": true, 121 | "variable-name": { 122 | "options": [ 123 | "ban-keywords", 124 | "check-format", 125 | "allow-pascal-case" 126 | ] 127 | }, 128 | "whitespace": { 129 | "options": [ 130 | "check-branch", 131 | "check-decl", 132 | "check-operator", 133 | "check-separator", 134 | "check-type", 135 | "check-typecast" 136 | ] 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "esModuleInterop": true, 8 | "declaration": false, 9 | "experimentalDecorators": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "target": "ES2022", 13 | "module": "es2020", 14 | "lib": [ 15 | "es2020", 16 | "dom" 17 | ], 18 | "paths": { 19 | "angular-google-tag-manager": [ 20 | "dist/angular-google-tag-manager/angular-google-tag-manager", 21 | "dist/angular-google-tag-manager" 22 | ] 23 | }, 24 | "useDefineForClassFields": false 25 | }, 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "skipTemplateCodegen": true, 29 | "strictMetadataEmit": true, 30 | "fullTemplateTypeCheck": true, 31 | "strictInjectionParameters": true, 32 | "enableResourceInlining": true 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "align": { 8 | "options": [ 9 | "parameters", 10 | "statements" 11 | ] 12 | }, 13 | "array-type": false, 14 | "arrow-return-shorthand": true, 15 | "curly": true, 16 | "deprecation": { 17 | "severity": "warning" 18 | }, 19 | "eofline": true, 20 | "import-blacklist": [ 21 | true, 22 | "rxjs/Rx" 23 | ], 24 | "import-spacing": true, 25 | "indent": { 26 | "options": [ 27 | "spaces" 28 | ] 29 | }, 30 | "max-classes-per-file": false, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-console": [ 47 | true, 48 | "debug", 49 | "info", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-empty": false, 55 | "no-inferrable-types": [ 56 | true, 57 | "ignore-params" 58 | ], 59 | "no-non-null-assertion": true, 60 | "no-redundant-jsdoc": true, 61 | "no-switch-case-fall-through": true, 62 | "no-var-requires": false, 63 | "object-literal-key-quotes": [ 64 | true, 65 | "as-needed" 66 | ], 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "semicolon": { 72 | "options": [ 73 | "always" 74 | ] 75 | }, 76 | "space-before-function-paren": { 77 | "options": { 78 | "anonymous": "never", 79 | "asyncArrow": "always", 80 | "constructor": "never", 81 | "method": "never", 82 | "named": "never" 83 | } 84 | }, 85 | "typedef": [ 86 | true, 87 | "call-signature" 88 | ], 89 | "typedef-whitespace": { 90 | "options": [ 91 | { 92 | "call-signature": "nospace", 93 | "index-signature": "nospace", 94 | "parameter": "nospace", 95 | "property-declaration": "nospace", 96 | "variable-declaration": "nospace" 97 | }, 98 | { 99 | "call-signature": "onespace", 100 | "index-signature": "onespace", 101 | "parameter": "onespace", 102 | "property-declaration": "onespace", 103 | "variable-declaration": "onespace" 104 | } 105 | ] 106 | }, 107 | "variable-name": { 108 | "options": [ 109 | "ban-keywords", 110 | "check-format", 111 | "allow-pascal-case" 112 | ] 113 | }, 114 | "whitespace": { 115 | "options": [ 116 | "check-branch", 117 | "check-decl", 118 | "check-operator", 119 | "check-separator", 120 | "check-type", 121 | "check-typecast" 122 | ] 123 | }, 124 | "component-class-suffix": true, 125 | "contextual-lifecycle": true, 126 | "directive-class-suffix": true, 127 | "no-conflicting-lifecycle": true, 128 | "no-host-metadata-property": true, 129 | "no-input-rename": true, 130 | "no-inputs-metadata-property": true, 131 | "no-output-native": true, 132 | "no-output-on-prefix": true, 133 | "no-output-rename": true, 134 | "no-outputs-metadata-property": true, 135 | "template-banana-in-box": true, 136 | "template-no-negated-async": true, 137 | "use-lifecycle-interface": true, 138 | "use-pipe-transform-interface": true 139 | } 140 | } 141 | --------------------------------------------------------------------------------