├── .browserslistrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── publish.yml │ └── tests.yml ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── package-lock.json ├── package.json ├── projects └── ngx-google-analytics │ ├── karma.conf.js │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── index.ts │ ├── lib │ │ ├── directives │ │ │ ├── ga-event-category.directive.spec.ts │ │ │ ├── ga-event-category.directive.ts │ │ │ ├── ga-event-form-input.directive.spec.ts │ │ │ ├── ga-event-form-input.directive.ts │ │ │ ├── ga-event.directive.spec.ts │ │ │ └── ga-event.directive.ts │ │ ├── enums │ │ │ └── ga-action.enum.ts │ │ ├── initializers │ │ │ ├── google-analytics-router.initializer.spec.ts │ │ │ ├── google-analytics-router.initializer.ts │ │ │ └── google-analytics.initializer.ts │ │ ├── interfaces │ │ │ ├── i-google-analytics-command.ts │ │ │ ├── i-google-analytics-routing-settings.ts │ │ │ └── i-google-analytics-settings.ts │ │ ├── ngx-google-analytics-router │ │ │ └── ngx-google-analytics-router.module.ts │ │ ├── ngx-google-analytics.module.ts │ │ ├── services │ │ │ ├── google-analytics.service.spec.ts │ │ │ └── google-analytics.service.ts │ │ ├── tokens │ │ │ ├── ngx-data-layer-token.ts │ │ │ ├── ngx-google-analytics-router-settings-token.ts │ │ │ ├── ngx-google-analytics-settings-token.ts │ │ │ ├── ngx-google-analytics-window.ts │ │ │ ├── ngx-gtag-token.ts │ │ │ └── ngx-window-token.ts │ │ └── types │ │ │ ├── data-layer.type.ts │ │ │ ├── ga-action.type.ts │ │ │ └── gtag.type.ts │ ├── public_api.ts │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ └── tsconfig.spec.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── test-page-a │ │ ├── test-page-a.component.css │ │ ├── test-page-a.component.html │ │ ├── test-page-a.component.spec.ts │ │ └── test-page-a.component.ts │ └── test-page-b │ │ ├── test-page-b.component.css │ │ ├── test-page-b.component.html │ │ ├── test-page-b.component.spec.ts │ │ └── test-page-b.component.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts ├── tsconfig.app.json └── tsconfig.spec.json └── tsconfig.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://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 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": [ 4 | "projects/**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "parserOptions": { 12 | "project": [ 13 | "tsconfig.json", 14 | "e2e/tsconfig.json" 15 | ], 16 | "createDefaultProgram": true 17 | }, 18 | "extends": [ 19 | "plugin:@angular-eslint/recommended", 20 | "plugin:@angular-eslint/template/process-inline-templates" 21 | ], 22 | "rules": { 23 | "@angular-eslint/component-selector": [ 24 | "error", 25 | { 26 | "prefix": "ga", 27 | "style": "kebab-case", 28 | "type": "element" 29 | } 30 | ], 31 | "@angular-eslint/directive-selector": [ 32 | "error", 33 | { 34 | "prefix": "ga", 35 | "style": "camelCase", 36 | "type": "attribute" 37 | } 38 | ] 39 | } 40 | }, 41 | { 42 | "files": [ 43 | "*.html" 44 | ], 45 | "extends": [ 46 | "plugin:@angular-eslint/template/recommended" 47 | ], 48 | "rules": {} 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Bump and Publish 5 | 6 | on: 7 | release: 8 | types: [released] 9 | # refs/tags/x.x.x 10 | 11 | jobs: 12 | tests: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: 16 19 | - name: 'Cache node Modules' 20 | uses: actions/cache@v1 21 | with: 22 | path: ~.npm 23 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 24 | restore-keys: | 25 | ${{ runner.os }}-node- 26 | - run: npm ci --ignore-scripts 27 | - run: npm test 28 | 29 | bump-and-build: 30 | needs: tests 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Get the version 34 | id: get_version 35 | run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} 36 | - uses: actions/checkout@v2 37 | - uses: actions/setup-node@v1 38 | with: 39 | node-version: 16 40 | registry-url: https://registry.npmjs.org/ 41 | - name: 'Cache node Modules' 42 | uses: actions/cache@v1 43 | with: 44 | path: ~.npm 45 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 46 | restore-keys: | 47 | ${{ runner.os }}-node- 48 | - run: npm ci --ignore-scripts 49 | - run: npm --no-git-tag-version --allow-same-version version ${{ steps.get_version.outputs.VERSION }} 50 | - run: npm --no-git-tag-version --allow-same-version version ${{ steps.get_version.outputs.VERSION }} 51 | working-directory: projects/ngx-google-analytics 52 | # - name: 'Commit Bump to ${{ steps.get_version.outputs.VERSION }}' 53 | # run: | 54 | # echo git config --local user.email "action@github.com" 55 | # echo git config --local user.name "GitHub Action" 56 | # git add package.json 57 | # git add projects/ngx-google-analytics/package.json 58 | # git status 59 | # git commit -m "Bump to ${{ steps.get_version.outputs.VERSION }} [skip ci]" 60 | # echo git push 61 | - run: npm run build 62 | - run: | 63 | cp LICENSE dist/ngx-google-analytics 64 | cp README.md dist/ngx-google-analytics 65 | cp CHANGELOG.md dist/ngx-google-analytics 66 | - run: npm pack 67 | working-directory: dist/ngx-google-analytics 68 | - name: 'Upload Build Artifatc ${{ steps.get_version.outputs.VERSION }}' 69 | uses: actions/upload-artifact@v2 70 | with: 71 | name: ${{ steps.get_version.outputs.VERSION }}.tgz 72 | path: dist/ngx-google-analytics/ngx-google-analytics-${{ steps.get_version.outputs.VERSION }}.tgz 73 | 74 | publish-npm: 75 | needs: bump-and-build 76 | runs-on: ubuntu-latest 77 | steps: 78 | - name: Get the version 79 | id: get_version 80 | run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} 81 | - name: Download a Build Artifact 82 | uses: actions/download-artifact@v2 83 | with: 84 | name: ${{ steps.get_version.outputs.VERSION }}.tgz 85 | - uses: actions/setup-node@v1 86 | with: 87 | node-version: 16 88 | registry-url: https://registry.npmjs.org/ 89 | - run: npm publish ngx-google-analytics-${{ steps.get_version.outputs.VERSION }}.tgz 90 | env: 91 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 92 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Build and Tests 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: 10 | - master 11 | - releases/* 12 | pull_request: 13 | branches: 14 | - master 15 | - releases/* 16 | 17 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 18 | jobs: 19 | run-tests: 20 | # The type of runner that the job will run on 21 | if: "!contains(github.event.head_commit.message, 'skip ci')" 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v2 27 | - uses: actions/setup-node@v1 28 | with: 29 | node-version: '16' 30 | registry-url: 'https://registry.npmjs.org' 31 | 32 | - name: 'Cache node Modules' 33 | uses: actions/cache@v1 34 | with: 35 | path: ~.npm 36 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 37 | restore-keys: | 38 | ${{ runner.os }}-node- 39 | 40 | - run: npm ci --ignore-scripts 41 | env: 42 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | 44 | - name: Build Angular Library 45 | run: npm run build 46 | 47 | - name: Running Tests 48 | run: npm run test 49 | -------------------------------------------------------------------------------- /.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 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.angular/cache 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | yarn-error.log 35 | testem.log 36 | /typings 37 | 38 | # System Files 39 | .DS_Store 40 | Thumbs.db 41 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "tslint.autoFixOnSave": true, 3 | "editor.detectIndentation": false, 4 | "editor.tabSize": 2, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.tslint": true 7 | } 8 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | * [14.0.0](#14.0.0) 4 | * [13.0.1](#13.0.1) 5 | * [13.0.0](#13.0.0) 6 | * [12.0.0](#12.0.0) 7 | * [11.2.1](#11.2.1) 8 | * [11.2.0](#11.2.0) 9 | * [11.1.0](#11.1.0) 10 | * [11.0.0](#11.0.0) 11 | * [10.0.0](#10.0.0) 12 | * [9.2.0](#9.2.0) 13 | * [9.1.1](#9.1.1) 14 | * [9.1.0](#9.1.0) 15 | * [9.0.1](#9.0.1) 16 | * [9.0.0](#9.0.0) 17 | * [8.1.0](#8.1.0) 18 | * [8.0.0](#8.0.0) 19 | 20 | ## DISCLAIMER 21 | 22 | I open my heart to share this component w/ you guys, buy I don't have much free time to keep this project always up to date, so if you find a Bug or a freek behaviour, please, fell free to open de source code and submit a PR to help yourself and other guys that use this lib too. :) 23 | 24 | I will upgrade this pack to any angular major version as soon as possible. Unfortunately I can't replicate new features to old compatibility versions. But you can fork this repo and port does features. 25 | 26 | ## 14.0.0 27 | * Adding additional optional parameters to allow for more rob… 28 | * Update to support angular 14 (#96) 29 | 30 | ## 13.0.1 31 | * Bump Karma 32 | * Bump Jasmine 33 | * Bump RXJS to 7.4.0 34 | * Migrate from TSLint to ESLint 35 | 36 | ## 13.0.0 37 | * Bump to ng v13 38 | 39 | ## 12.0.0 40 | * Bump to ng v12 41 | 42 | ## 11.2.1 43 | 44 | * Allow override initial commands 45 | 46 | ## 11.2.0 47 | 48 | * Fixed parameter initCommands on NgxGoogleAnalyticsModule.forRoot() #46 49 | * Allow directive gaBind to trigger on any kind of event. #43 50 | 51 | ## 11.1.0 52 | 53 | * Using enum instead of string type (#38) 54 | 55 | ## 11.0.0 56 | 57 | * Bump to ng v11 58 | 59 | ## 10.0.0 60 | 61 | * Bump to ng v10 62 | 63 | ## 9.2.0 64 | 65 | * Add include/exclude rules feature on NgxGoogleAnalyticsRouterModule.forRoot() to filter witch pages should trigger page-view event. 66 | * Remove `peerDependencies` from package.json to do not trigger unnecessary warnings on `npm install` command. 67 | 68 | ## 9.1.1 69 | 70 | * [Bugfix] Set nonce using `setAttribute` 71 | 72 | ## 9.1.0 73 | 74 | * Add nonce 75 | * Fix typos 76 | * Rename i-google-analytics-command.ts 77 | 78 | ## 9.0.1 79 | 80 | * Created set() method at GoogleAnalyticsService (https://developers.google.com/analytics/devguides/collection/gtagjs/setting-values); 81 | * Changed gtag() method signature at GoogleAnalyticsService to acept anything; 82 | * Added a filter to remove undefined values to rest parameter on gtag() fn; 83 | 84 | ## 9.0.0 85 | 86 | Just bump to Angular ^9.x 87 | 88 | ## 8.1.0 89 | 90 | I finally get some time this weekend and decide to work on some unfinished issues. there it go: 91 | 92 | * Created and Updated unit tests on library project; 93 | * Created an automated workflow to run unit tests on each PR; 94 | * Created TypeDocs on all Services, Modules and Directives to help you guys to use this lib; 95 | * Removed bad practices on access Window and Document objects directily by Angular Services. I decided to create Injection Tokens to resolve does Broser Objects.; 96 | * Added some validations to ensure it is a Browser Environment; 97 | * Added cleanup code on NgxGoogleAnalyticsRouterModule. In short, we now unsubscribe Router events when bootstrap app is destroied; 98 | * Added a new Settings property `ennableTracing` to log on console Errors and Warnings about `gtag()` calls; 99 | * Now we have `InjectionToken` for everything. You can replace all our default settings; 100 | 101 | ## 8.0.0 102 | 103 | Sorry, I don't have time to catalog all changes done on the previous versions. You can get a detailed (😂) description of each previous versions on GitHub releases and commit histories. Don't worry, there are nothing still relevant there. 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Max 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 | # Ngx Google Analytics 2 | 3 | An easy implementation to track ga on angular8+ apps. 4 | 5 | Feedbacks on https://github.com/maxandriani/ngx-google-analytics 6 | 7 | ![Build and Tests](https://github.com/maxandriani/ngx-google-analytics/workflows/Build%20and%20Tests/badge.svg) 8 | 9 | # Notice 10 | 11 | I'm investing a big amount of time studing new technologies for my daily job, and I am not able to invest a significant amount of time into maintaining `ngx-google-analytics` properly. I am looking for volunteers who would like to become active maintainers on the project. If you are interested, please shoot me a note. 12 | 13 | # Index 14 | 15 | * [Setup](#setup) 16 | * [NPM](#npm) 17 | * [Simple Setup](#simple-setup) 18 | * [Routing Setup](#setup-routing-module) 19 | * [Advanced Routing Setup](#advanced-setup-routing-module) 20 | * [GoogleAnalyticsService](#googleanalyticsservice) 21 | * [Directives](#directives) 22 | * [Changelog](CHANGELOG.md) 23 | 24 | ## Setup 25 | 26 | ### NPM 27 | 28 | To setup this package on you project, just call the following command. 29 | 30 | ``` 31 | npm install ngx-google-analytics 32 | ``` 33 | 34 | ### Simple Setup 35 | 36 | On your Angular Project, you shall include the `NgxGoogleAnalyticsModule` on your highest level application module. ie `AddModule`. The easiest install mode call the `forRoot()` method and pass the GA tracking code. 37 | 38 | ```ts 39 | import { NgxGoogleAnalyticsModule } from 'ngx-google-analytics'; 40 | 41 | @NgModule({ 42 | declarations: [ 43 | AppComponent 44 | ], 45 | imports: [ 46 | BrowserModule, 47 | NgxGoogleAnalyticsModule.forRoot('traking-code') 48 | // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 49 | ], 50 | providers: [], 51 | bootstrap: [AppComponent] 52 | }) 53 | export class AppModule { } 54 | ``` 55 | 56 | ### Setup Routing Module 57 | 58 | We provide a second Module Dependency to configure Router Event Bindings and perform automatic page view every time your application navigates to another page. 59 | 60 | Add ```NgxGoogleAnalyticsRouterModule``` on AppModule enable auto track `Router` events. 61 | 62 | **IMPORTANT:** This Module just subscribe to Router events when the bootstrap component is created, and then cleans up any subscriptions related to previous component when it is destroyed. You may get some issues if using this module on a server side rendering or multiple bootstrap components. If it is your case, I suggest you subscribe to events by yourself. You can use git repository as reference. 63 | 64 | ```ts 65 | import { NgxGoogleAnalyticsModule, NgxGoogleAnalyticsRouterModule } from 'ngx-google-analytics'; 66 | ... 67 | 68 | @NgModule({ 69 | ... 70 | imports: [ 71 | ... 72 | NgxGoogleAnalyticsModule.forRoot(environment.ga), 73 | NgxGoogleAnalyticsRouterModule 74 | // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 75 | ] 76 | }) 77 | export class AppModule {} 78 | ``` 79 | 80 | ### Advanced Setup Routing Module 81 | 82 | You can customize some rules to include/exclude routes on `NgxGoogleAnalyticsRouterModule`. The include/exclude settings allow: 83 | * Simple route match: `{ include: [ '/full-uri-match' ] }`; 84 | * Wildcard route match: `{ include: [ '*/public/*' ] }`; 85 | * Regular Expression route match: `{ include: [ /^\/public\/.*/ ] }`; 86 | 87 | ```ts 88 | import { NgxGoogleAnalyticsModule, NgxGoogleAnalyticsRouterModule } from 'ngx-google-analytics'; 89 | ... 90 | 91 | @NgModule({ 92 | ... 93 | imports: [ 94 | ... 95 | NgxGoogleAnalyticsModule.forRoot(environment.ga), 96 | NgxGoogleAnalyticsRouterModule.forRoot({ include: [...], exclude: [...] }) 97 | // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 98 | ] 99 | }) 100 | export class AppModule {} 101 | ``` 102 | 103 | 104 | ## GoogleAnalyticsService 105 | 106 | This service provides an easy and strong typed way to call `gtag()` command. It does nothing else then convert a strong typed list of arguments into a standard `gtag` api call. 107 | 108 | ### Call Interface Events 109 | 110 | ```ts 111 | @Component( ... ) 112 | export class TestFormComponent { 113 | 114 | constructor( 115 | private $gaService: GoogleAnalyticsService 116 | ) {} 117 | 118 | onUserInputName() { 119 | ... 120 | this.$gaService.event('enter_name', 'user_register_form', 'Name'); 121 | } 122 | 123 | onUserInputEmail() { 124 | ... 125 | this.$gaService.event('enter_email', 'user_register_form', 'Email'); 126 | } 127 | 128 | onSubmit() { 129 | ... 130 | this.$gaService.event('submit', 'user_register_form', 'Enviar'); 131 | } 132 | 133 | } 134 | ``` 135 | 136 | ### Call GA Page Views and Virtual Page Views 137 | 138 | ```ts 139 | @Component(...) 140 | export class TestPageComponent implements OnInit { 141 | 142 | constructor( 143 | protected $gaService: GoogleAnalyticsService 144 | ) {} 145 | 146 | ngOnInit() { 147 | this.$gaService.pageView('/teste', 'Teste de Title') 148 | } 149 | 150 | onUserLogin() { 151 | ... 152 | this.$gaService.pageView('/teste', 'Teste de Title', undefined, { 153 | user_id: 'my-user-id' 154 | }) 155 | } 156 | 157 | } 158 | ``` 159 | 160 | ## Directives 161 | 162 | In a way to help you to be more productive on attach GA events on UI elements. We create some directives to handle `GoogleAnalyticsService` and add event listener by simple attributes. 163 | 164 | ### Simple directive use 165 | 166 | The default behaviour is call `gtag` on click events, but you can change the trigger to any HTML Event (e.g. `focus`, `blur` or custom events) as well. 167 | 168 | ```js 169 |
170 | 171 | 172 | 173 | 174 |
175 | ``` 176 | 177 | ### Simple input use 178 | 179 | If you attach gaEvent directive on form elements, it will assume focus event as default `trigger`. 180 | 181 | ```js 182 |
183 | 184 |
185 | ``` 186 | 187 | ### Grouped directives 188 | 189 | Sometimes your UX guy want to group several elements in the interface at same group to help his analysis and reports. Fortunately the `gaCategory` directive can be placed on the highest level group 190 | element and all child `gaEvent` will assume the parent `gaCategory` as their parent. 191 | 192 | ```js 193 |
194 | 195 | 196 | 197 |
198 | ``` 199 | 200 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngx-google-analytics-sdk": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/ngx-google-analytics-sdk", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "src/tsconfig.app.json", 21 | "assets": [ 22 | "src/favicon.ico", 23 | "src/assets" 24 | ], 25 | "styles": [ 26 | "src/styles.css" 27 | ], 28 | "scripts": [], 29 | "vendorChunk": true, 30 | "extractLicenses": false, 31 | "buildOptimizer": false, 32 | "sourceMap": true, 33 | "optimization": false, 34 | "namedChunks": true 35 | }, 36 | "configurations": { 37 | "production": { 38 | "budgets": [ 39 | { 40 | "type": "anyComponentStyle", 41 | "maximumWarning": "6kb" 42 | } 43 | ], 44 | "fileReplacements": [ 45 | { 46 | "replace": "src/environments/environment.ts", 47 | "with": "src/environments/environment.prod.ts" 48 | } 49 | ], 50 | "optimization": true, 51 | "outputHashing": "all", 52 | "sourceMap": false, 53 | "namedChunks": false, 54 | "extractLicenses": true, 55 | "vendorChunk": false, 56 | "buildOptimizer": true 57 | }, 58 | "development": {} 59 | }, 60 | "defaultConfiguration": "production" 61 | }, 62 | "serve": { 63 | "builder": "@angular-devkit/build-angular:dev-server", 64 | "options": {}, 65 | "configurations": { 66 | "production": { 67 | "browserTarget": "ngx-google-analytics-sdk:build:production" 68 | }, 69 | "development": { 70 | "browserTarget": "ngx-google-analytics-sdk:build:development" 71 | } 72 | }, 73 | "defaultConfiguration": "development" 74 | }, 75 | "extract-i18n": { 76 | "builder": "@angular-devkit/build-angular:extract-i18n", 77 | "options": { 78 | "browserTarget": "ngx-google-analytics-sdk:build" 79 | } 80 | }, 81 | "test": { 82 | "builder": "@angular-devkit/build-angular:karma", 83 | "options": { 84 | "main": "src/test.ts", 85 | "polyfills": "src/polyfills.ts", 86 | "tsConfig": "src/tsconfig.spec.json", 87 | "karmaConfig": "src/karma.conf.js", 88 | "styles": [ 89 | "src/styles.css" 90 | ], 91 | "scripts": [], 92 | "assets": [ 93 | "src/favicon.ico", 94 | "src/assets" 95 | ] 96 | } 97 | } 98 | } 99 | }, 100 | "ngx-google-analytics-sdk-e2e": { 101 | "root": "e2e/", 102 | "projectType": "application", 103 | "architect": { 104 | "e2e": { 105 | "builder": "@angular-devkit/build-angular:protractor", 106 | "options": { 107 | "protractorConfig": "e2e/protractor.conf.js" 108 | }, 109 | "configurations": { 110 | "production": { 111 | "devServerTarget": "ngx-google-analytics-sdk:serve:production" 112 | }, 113 | "development": { 114 | "devServerTarget": "ngx-google-analytics-sdk:serve:development" 115 | } 116 | }, 117 | "defaultConfiguration": "development" 118 | }, 119 | "lint": { 120 | "builder": "@angular-eslint/builder:lint", 121 | "options": { 122 | "lintFilePatterns": [ 123 | "src/**/*.ts", 124 | "src/**/*.html" 125 | ], 126 | "tsConfig": "e2e/tsconfig.e2e.json", 127 | "exclude": [ 128 | "**/node_modules/**" 129 | ] 130 | } 131 | } 132 | } 133 | }, 134 | "ngx-google-analytics": { 135 | "root": "projects/ngx-google-analytics", 136 | "sourceRoot": "projects/ngx-google-analytics/src", 137 | "projectType": "library", 138 | "prefix": "ga", 139 | "architect": { 140 | "build": { 141 | "builder": "@angular-devkit/build-angular:ng-packagr", 142 | "options": { 143 | "tsConfig": "projects/ngx-google-analytics/tsconfig.lib.json", 144 | "project": "projects/ngx-google-analytics/ng-package.json" 145 | }, 146 | "configurations": { 147 | "production": { 148 | "tsConfig": "projects/ngx-google-analytics/tsconfig.lib.prod.json" 149 | }, 150 | "development": {} 151 | }, 152 | "defaultConfiguration": "production" 153 | }, 154 | "test": { 155 | "builder": "@angular-devkit/build-angular:karma", 156 | "options": { 157 | "main": "projects/ngx-google-analytics/src/test.ts", 158 | "tsConfig": "projects/ngx-google-analytics/tsconfig.spec.json", 159 | "karmaConfig": "projects/ngx-google-analytics/karma.conf.js" 160 | } 161 | }, 162 | "lint": { 163 | "builder": "@angular-eslint/builder:lint", 164 | "options": { 165 | "lintFilePatterns": [ 166 | "src/**/*.ts", 167 | "src/**/*.html" 168 | ] 169 | } 170 | } 171 | } 172 | } 173 | }, 174 | "defaultProject": "ngx-google-analytics-sdk", 175 | "cli": { 176 | "analytics": "3ce39043-c695-45ae-9eeb-07fed5fb3a2e", 177 | "defaultCollection": "@angular-eslint/schematics" 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | const {StacktraceOption} = require("jasmine-spec-reporter/built/configuration"); 6 | 7 | exports.config = { 8 | allScriptsTimeout: 11000, 9 | specs: [ 10 | './src/**/*.e2e-spec.ts' 11 | ], 12 | capabilities: { 13 | 'browserName': 'chrome' 14 | }, 15 | directConnect: true, 16 | baseUrl: 'http://localhost:4200/', 17 | framework: 'jasmine', 18 | jasmineNodeOpts: { 19 | showColors: true, 20 | defaultTimeoutInterval: 30000, 21 | print: function() {} 22 | }, 23 | onPrepare() { 24 | require('ts-node').register({ 25 | project: require('path').join(__dirname, './tsconfig.e2e.json') 26 | }); 27 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: StacktraceOption.PRETTY } })); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to ngx-google-analytics-sdk!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-google-analytics-sdk", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "prestart": "npm run build", 7 | "start": "ng serve", 8 | "build": "ng build ngx-google-analytics", 9 | "test": "ng test ngx-google-analytics --browsers=ChromeHeadless --watch=false", 10 | "test:dev": "ng test ngx-google-analytics", 11 | "lint": "ng lint ngx-google-analytics", 12 | "e2e": "ng e2e" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/animations": "^14.0.2", 17 | "@angular/cli": "~14.0.2", 18 | "@angular/common": "^14.0.2", 19 | "@angular/compiler": "^14.0.2", 20 | "@angular/core": "^14.0.2", 21 | "@angular/forms": "^14.0.2", 22 | "@angular/platform-browser": "^14.0.2", 23 | "@angular/platform-browser-dynamic": "^14.0.2", 24 | "@angular/router": "^14.0.2", 25 | "rxjs": "~7.5.5", 26 | "tslib": "^2.4.0", 27 | "zone.js": "~0.11.6" 28 | }, 29 | "devDependencies": { 30 | "@angular-devkit/build-angular": "~14.0.2", 31 | "@angular-devkit/core": "^14.0.2", 32 | "@angular-devkit/schematics": "^14.0.2", 33 | "@angular-eslint/builder": "^14.0.0-alpha.3", 34 | "@angular-eslint/eslint-plugin": "^14.0.0-alpha.3", 35 | "@angular-eslint/eslint-plugin-template": "^14.0.0-alpha.3", 36 | "@angular-eslint/schematics": "^14.0.0-alpha.3", 37 | "@angular-eslint/template-parser": "^14.0.0-alpha.3", 38 | "@angular/compiler-cli": "^14.0.2", 39 | "@angular/language-service": "^14.0.2", 40 | "@types/jasmine": "~4.0.3", 41 | "@types/jasminewd2": "~2.0.10", 42 | "@types/node": "^17.0.44", 43 | "@typescript-eslint/eslint-plugin": "5.28.0", 44 | "@typescript-eslint/parser": "5.28.0", 45 | "codelyzer": "^6.0.2", 46 | "eslint": "^8.17.0", 47 | "jasmine-core": "~4.2.0", 48 | "jasmine-spec-reporter": "~7.0.0", 49 | "karma": "~6.4.0", 50 | "karma-chrome-launcher": "~3.1.1", 51 | "karma-coverage-istanbul-reporter": "~3.0.3", 52 | "karma-jasmine": "~5.0.1", 53 | "karma-jasmine-html-reporter": "^2.0.0", 54 | "ng-packagr": "^14.0.2", 55 | "protractor": "~7.0.0", 56 | "ts-node": "~10.8.1", 57 | "tslint": "~6.1.3", 58 | "typescript": "~4.7.3" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/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'), 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 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ngx-google-analytics", 4 | "lib": { 5 | "entryFile": "src/public_api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /projects/ngx-google-analytics/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-google-analytics", 3 | "version": "14.0.0", 4 | "description": "A simple ng-9 wrapper to load Google Analytics dependency by angular way", 5 | "keywords": [ "google", "google analytics", "angular", "angular-9+", "typescript" ], 6 | "homepage": "https://github.com/maxandriani/ngx-google-analytics", 7 | "license": "MIT", 8 | "dependencies": { 9 | "tslib": "^2.4.0" 10 | }, 11 | "peerDependencies": { 12 | "@angular/common": ">=12.0.0", 13 | "@angular/core": ">=12.0.0" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/maxandriani/ngx-google-analytics" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Fix the anoing problem when you try to use this library on the sabe enviroment and VSCode always user public_api path to import files. 3 | */ 4 | export * from './public_api'; 5 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/directives/ga-event-category.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { GaEventCategoryDirective } from './ga-event-category.directive'; 2 | 3 | describe('GaEventCategoryDirective', () => { 4 | it('should create an instance', () => { 5 | const directive = new GaEventCategoryDirective(); 6 | expect(directive).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/directives/ga-event-category.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: `[gaEvent][gaCategory], 5 | [gaCategory]`, 6 | exportAs: 'gaCategory' 7 | }) 8 | export class GaEventCategoryDirective { 9 | 10 | constructor() { } 11 | 12 | @Input() gaCategory!: string; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/directives/ga-event-form-input.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { GaEventFormInputDirective } from './ga-event-form-input.directive'; 2 | import { TestBed, ComponentFixture } from '@angular/core/testing'; 3 | import { NgxGoogleAnalyticsModule } from '../ngx-google-analytics.module'; 4 | import { GaEventDirective } from './ga-event.directive'; 5 | import { GaEventCategoryDirective } from './ga-event-category.directive'; 6 | import { GoogleAnalyticsService } from '../services/google-analytics.service'; 7 | import { Component } from '@angular/core'; 8 | import { NGX_GOOGLE_ANALYTICS_SETTINGS_TOKEN } from '../tokens/ngx-google-analytics-settings-token'; 9 | 10 | describe('GaEventFormInputDirective', () => { 11 | 12 | @Component({ 13 | selector: 'ga-host', 14 | template: `` 15 | }) 16 | class HostComponent {} 17 | 18 | let gaEventFormInput: GaEventFormInputDirective, 19 | gaEvent: GaEventDirective, 20 | gaCategory: GaEventCategoryDirective, 21 | host: HostComponent, 22 | fixture: ComponentFixture; 23 | 24 | beforeEach(() => { 25 | TestBed.configureTestingModule({ 26 | imports: [ 27 | NgxGoogleAnalyticsModule 28 | ], 29 | declarations: [ 30 | HostComponent 31 | ] 32 | }).compileComponents(); 33 | }); 34 | 35 | beforeEach(() => { 36 | fixture = TestBed.createComponent(HostComponent); 37 | host = fixture.componentInstance; 38 | fixture.detectChanges(); 39 | }); 40 | 41 | beforeEach(() => { 42 | gaCategory = new GaEventCategoryDirective(); 43 | gaEvent = new GaEventDirective(gaCategory, TestBed.inject(GoogleAnalyticsService), TestBed.inject(NGX_GOOGLE_ANALYTICS_SETTINGS_TOKEN), fixture.elementRef); 44 | gaEventFormInput = new GaEventFormInputDirective(gaEvent); 45 | }); 46 | 47 | it('should create an instance', () => { 48 | expect(gaEventFormInput).toBeTruthy(); 49 | }); 50 | 51 | it('should update gaBind when input is updated', () => { 52 | gaEventFormInput.gaBind = 'click'; 53 | expect(gaEvent.gaBind).toBe('click'); 54 | }); 55 | 56 | it('should use `focus` as a default gaBind', () => { 57 | expect(gaEvent.gaBind).toBe('focus'); 58 | }); 59 | 60 | it('should call `GoogleAnalyticsService.event()` on trigger focus at input', () => { 61 | const ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService), 62 | spyOnGa = spyOn(ga, 'event'), 63 | input = fixture.debugElement.query(e => e.name === 'input'); 64 | 65 | fixture.detectChanges(); 66 | input.nativeElement.dispatchEvent(new FocusEvent('focus')); 67 | fixture.detectChanges(); 68 | 69 | expect(spyOnGa).toHaveBeenCalledWith('teste', undefined, undefined, undefined, undefined); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/directives/ga-event-form-input.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Host, Optional, Input } from '@angular/core'; 2 | import { GaEventDirective } from './ga-event.directive'; 3 | 4 | @Directive({ 5 | selector: `input[gaEvent], 6 | select[gaEvent], 7 | textarea[gaEvent]` 8 | }) 9 | export class GaEventFormInputDirective { 10 | 11 | constructor( 12 | @Host() @Optional() protected gaEvent: GaEventDirective 13 | ) { 14 | this.gaBind = 'focus'; 15 | } 16 | 17 | @Input() set gaBind(bind: string) { 18 | if (this.gaEvent) { 19 | this.gaEvent.gaBind = bind; 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/directives/ga-event.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { GaEventDirective } from './ga-event.directive'; 2 | import { TestBed, ComponentFixture } from '@angular/core/testing'; 3 | import { NgxGoogleAnalyticsModule } from '../ngx-google-analytics.module'; 4 | import { GoogleAnalyticsService } from '../services/google-analytics.service'; 5 | import { Component } from '@angular/core'; 6 | import { GaActionEnum } from '../enums/ga-action.enum'; 7 | 8 | describe('GaEventDirective', () => { 9 | 10 | @Component({ 11 | selector: 'ga-host', 12 | template: ` 13 | 21 | 30 | 39 | 48 | 57 | ` 58 | }) 59 | class HostComponent { 60 | gaAction: GaActionEnum | string; 61 | gaLabel: string; 62 | label: string; 63 | gaValue: number; 64 | gaInteraction: boolean; 65 | gaBind = 'click'; 66 | gaEvent: GaActionEnum | string; 67 | } 68 | 69 | let fixture: ComponentFixture, 70 | host: HostComponent; 71 | 72 | beforeEach(() => { 73 | TestBed.configureTestingModule({ 74 | imports: [ 75 | NgxGoogleAnalyticsModule 76 | ], 77 | declarations: [ 78 | HostComponent 79 | ] 80 | }).compileComponents(); 81 | }); 82 | 83 | beforeEach(() => { 84 | fixture = TestBed.createComponent(HostComponent); 85 | host = fixture.componentInstance; 86 | // gaCategory = fixture 87 | // .debugElement 88 | // .query(c => c.classes['test-4']) 89 | // .injector 90 | // .get(GaEventCategoryDirective); 91 | // gaEvent = new GaEventDirective(gaCategory, TestBed.inject(GoogleAnalyticsService)); 92 | }); 93 | 94 | it('should create an instance', () => { 95 | const gaEvent = fixture 96 | .debugElement 97 | .query(i => (i.nativeElement as HTMLButtonElement).classList.contains('test-1')) 98 | .injector 99 | .get(GaEventDirective); 100 | expect(gaEvent).toBeTruthy(); 101 | }); 102 | 103 | it('should call `trigger` on click event', () => { 104 | const ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService), 105 | spyOnGa = spyOn(ga, 'event'), 106 | input = fixture.debugElement.query(e => (e.nativeElement as HTMLButtonElement).classList.contains('test-click')); 107 | 108 | fixture.detectChanges(); 109 | input.nativeElement.click(); 110 | fixture.detectChanges(); 111 | 112 | expect(spyOnGa).toHaveBeenCalledWith('test-1', undefined, undefined, undefined, undefined); 113 | }); 114 | 115 | it('should call `trigger` on focus event', () => { 116 | const ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService), 117 | spyOnGa = spyOn(ga, 'event'), 118 | input = fixture.debugElement.query(e => (e.nativeElement as HTMLButtonElement).classList.contains('test-focus')); 119 | 120 | fixture.detectChanges(); 121 | input.nativeElement.dispatchEvent(new FocusEvent('focus')); 122 | fixture.detectChanges(); 123 | 124 | expect(spyOnGa).toHaveBeenCalledWith('test-2', undefined, undefined, undefined, undefined); 125 | }); 126 | 127 | it('should call `trigger on blur event`', () => { 128 | const ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService), 129 | spyOnGa = spyOn(ga, 'event'), 130 | input = fixture.debugElement.query(e => (e.nativeElement as HTMLButtonElement).classList.contains('test-blur')); 131 | 132 | fixture.detectChanges(); 133 | input.nativeElement.dispatchEvent(new FocusEvent('blur')); 134 | fixture.detectChanges(); 135 | 136 | expect(spyOnGa).toHaveBeenCalledWith('test-3', undefined, undefined, undefined, undefined); 137 | }); 138 | 139 | it('should call `trigger on custom event`', () => { 140 | const ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService), 141 | spyOnGa = spyOn(ga, 'event'), 142 | input = fixture.debugElement.query(e => (e.nativeElement as HTMLButtonElement).classList.contains('test-custom')); 143 | 144 | fixture.detectChanges(); 145 | input.nativeElement.dispatchEvent(new CustomEvent('custom')); 146 | fixture.detectChanges(); 147 | 148 | expect(spyOnGa).toHaveBeenCalledWith('test-5', undefined, undefined, undefined, undefined); 149 | }); 150 | 151 | it('should warn a message when try to call a event w/o gaEvent/gaAction value', () => { 152 | const ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService), 153 | spyOnConsole = spyOn(console, 'warn'), 154 | input = fixture.debugElement.query(e => (e.nativeElement as HTMLButtonElement).classList.contains('test-category')); 155 | 156 | fixture.detectChanges(); 157 | input.nativeElement.click(); 158 | fixture.detectChanges(); 159 | 160 | expect(spyOnConsole).toHaveBeenCalled(); 161 | }); 162 | 163 | it('should grab gaAction and pass to event trigger', () => { 164 | const ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService), 165 | action = 'action-t', 166 | spyOnGa = spyOn(ga, 'event'), 167 | input = fixture.debugElement.query(e => (e.nativeElement as HTMLButtonElement).classList.contains('test-category')); 168 | 169 | host.gaAction = action; 170 | fixture.detectChanges(); 171 | input.nativeElement.click(); 172 | fixture.detectChanges(); 173 | 174 | expect(spyOnGa).toHaveBeenCalledWith(action, 'test-4', undefined, undefined, undefined); 175 | }); 176 | 177 | it('should grab gaEvent and pass to event trigger', () => { 178 | const ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService), 179 | action = 'action-t', 180 | spyOnGa = spyOn(ga, 'event'), 181 | input = fixture.debugElement.query(e => (e.nativeElement as HTMLButtonElement).classList.contains('test-category')); 182 | 183 | host.gaEvent = action; 184 | fixture.detectChanges(); 185 | input.nativeElement.click(); 186 | fixture.detectChanges(); 187 | 188 | expect(spyOnGa).toHaveBeenCalledWith(action, 'test-4', undefined, undefined, undefined); 189 | }); 190 | 191 | it('should grab gaCategory and pass to event trigger', () => { 192 | const ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService), 193 | action = 'action-t', 194 | spyOnGa = spyOn(ga, 'event'), 195 | input = fixture.debugElement.query(e => (e.nativeElement as HTMLButtonElement).classList.contains('test-category')); 196 | 197 | host.gaAction = action; 198 | fixture.detectChanges(); 199 | input.nativeElement.click(); 200 | fixture.detectChanges(); 201 | 202 | expect(spyOnGa).toHaveBeenCalledWith(action, 'test-4', undefined, undefined, undefined); 203 | }); 204 | 205 | it('should grab gaLabel and pass to event trigger', () => { 206 | const ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService), 207 | action = 'action-t', 208 | label = 'label-t', 209 | spyOnGa = spyOn(ga, 'event'), 210 | input = fixture.debugElement.query(e => (e.nativeElement as HTMLButtonElement).classList.contains('test-category')); 211 | 212 | host.gaAction = action; 213 | host.gaLabel = label; 214 | fixture.detectChanges(); 215 | input.nativeElement.click(); 216 | fixture.detectChanges(); 217 | 218 | expect(spyOnGa).toHaveBeenCalledWith(action, 'test-4', label, undefined, undefined); 219 | }); 220 | 221 | it('should grab label and pass to event trigger', () => { 222 | const ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService), 223 | action = 'action-t', 224 | label = 'label-t', 225 | spyOnGa = spyOn(ga, 'event'), 226 | input = fixture.debugElement.query(e => (e.nativeElement as HTMLButtonElement).classList.contains('test-category')); 227 | 228 | host.gaAction = action; 229 | host.label = label; 230 | fixture.detectChanges(); 231 | input.nativeElement.click(); 232 | fixture.detectChanges(); 233 | 234 | expect(spyOnGa).toHaveBeenCalledWith(action, 'test-4', label, undefined, undefined); 235 | }); 236 | 237 | it('should grab gaValue and pass to event trigger', () => { 238 | const ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService), 239 | action = 'action-t', 240 | value = 40, 241 | spyOnGa = spyOn(ga, 'event'), 242 | input = fixture.debugElement.query(e => (e.nativeElement as HTMLButtonElement).classList.contains('test-category')); 243 | 244 | host.gaAction = action; 245 | host.gaValue = value; 246 | fixture.detectChanges(); 247 | input.nativeElement.click(); 248 | fixture.detectChanges(); 249 | 250 | expect(spyOnGa).toHaveBeenCalledWith(action, 'test-4', undefined, value, undefined); 251 | }); 252 | 253 | it('should grab gaInteraction and pass to event trigger', () => { 254 | const ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService), 255 | action = 'action-t', 256 | gaInteraction = true, 257 | spyOnGa = spyOn(ga, 'event'), 258 | input = fixture.debugElement.query(e => (e.nativeElement as HTMLButtonElement).classList.contains('test-category')); 259 | 260 | host.gaAction = action; 261 | host.gaInteraction = gaInteraction; 262 | fixture.detectChanges(); 263 | input.nativeElement.click(); 264 | fixture.detectChanges(); 265 | 266 | expect(spyOnGa).toHaveBeenCalledWith(action, 'test-4', undefined, undefined, gaInteraction); 267 | }); 268 | 269 | }); 270 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/directives/ga-event.directive.ts: -------------------------------------------------------------------------------- 1 | import {Directive, ElementRef, Inject, Input, isDevMode, OnDestroy, Optional} from '@angular/core'; 2 | import {GaEventCategoryDirective} from './ga-event-category.directive'; 3 | import {GoogleAnalyticsService} from '../services/google-analytics.service'; 4 | import {GaActionEnum} from '../enums/ga-action.enum'; 5 | import {NGX_GOOGLE_ANALYTICS_SETTINGS_TOKEN} from '../tokens/ngx-google-analytics-settings-token'; 6 | import {IGoogleAnalyticsSettings} from '../interfaces/i-google-analytics-settings'; 7 | import {fromEvent, Subscription} from 'rxjs'; 8 | 9 | @Directive({ 10 | selector: `[gaEvent]`, 11 | exportAs: 'gaEvent' 12 | }) 13 | export class GaEventDirective implements OnDestroy { 14 | 15 | constructor( 16 | @Optional() private gaCategoryDirective: GaEventCategoryDirective, 17 | private gaService: GoogleAnalyticsService, 18 | @Inject(NGX_GOOGLE_ANALYTICS_SETTINGS_TOKEN) private settings: IGoogleAnalyticsSettings, 19 | private readonly el: ElementRef 20 | ) { 21 | this.gaBind = 'click'; 22 | } 23 | 24 | private bindSubscription?: Subscription; 25 | 26 | @Input() gaAction!: GaActionEnum | string; 27 | @Input() gaLabel!: string; 28 | @Input() label!: string; 29 | @Input() gaValue!: number; 30 | @Input() gaInteraction!: boolean; 31 | @Input() gaEvent!: GaActionEnum | string; 32 | 33 | private _gaBind!: string; 34 | 35 | @Input() set gaBind (gaBind: string) { 36 | if (this.bindSubscription) { 37 | this.bindSubscription.unsubscribe(); 38 | } 39 | 40 | this._gaBind = gaBind; 41 | this.bindSubscription = fromEvent(this.el.nativeElement, gaBind).subscribe(() => this.trigger()); 42 | } 43 | get gaBind(): string { 44 | return this._gaBind; 45 | } 46 | 47 | ngOnDestroy() { 48 | if (this.bindSubscription) { 49 | this.bindSubscription.unsubscribe(); 50 | } 51 | } 52 | 53 | protected trigger() { 54 | try { 55 | // Observação: não é obrigatório especificar uma categoria, uma etiqueta ou um valor. Consulte Eventos padrão do Google Analytics abaixo. 56 | // if (!this.$gaCategoryDirective) { 57 | // throw new Error('You must provide a gaCategory attribute w/ gaEvent Directive.'); 58 | // } 59 | 60 | if (!this.gaAction && !this.gaEvent) { 61 | throw new Error('You must provide a gaAction attribute to identify this event.'); 62 | } 63 | 64 | this.gaService 65 | .event( 66 | this.gaAction || this.gaEvent, 67 | (this.gaCategoryDirective) ? this.gaCategoryDirective.gaCategory : undefined, 68 | this.gaLabel || this.label, 69 | this.gaValue, 70 | this.gaInteraction 71 | ); 72 | } catch (err: any) { 73 | this.throw(err); 74 | } 75 | } 76 | 77 | protected throw(err: Error) { 78 | if ((isDevMode() || this.settings.enableTracing) && console && console.warn) { 79 | console.warn(err); 80 | } 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/enums/ga-action.enum.ts: -------------------------------------------------------------------------------- 1 | export enum GaActionEnum { 2 | ADD_PAYMENT_INFO = 'add_payment_info', 3 | ADD_TO_CART = 'add_to_cart', 4 | ADD_TO_WISHLIST = 'add_to_wishlist', 5 | BEGIN_CHECKOUT = 'begin_checkout', 6 | CHECKOUT_PROGRESS = 'checkout_progress', 7 | GENERATE_LEAD = 'generate_lead', 8 | LOGIN = 'login', 9 | PURCHASE = 'purchase', 10 | REFUND = 'refund', 11 | REMOVE_FROM_CART = 'remove_from_cart', 12 | SEARCH = 'search', 13 | SELECT_CONTENT = 'select_content', 14 | SET_CHECKOUT_OPTION = 'set_checkout_option', 15 | SHARE = 'share', 16 | SIGN_UP = 'sign_up', 17 | VIEW_ITEM = 'view_item', 18 | VIEW_ITEM_LIST = 'view_item_list', 19 | VIEW_PROMOTION = 'view_promotion', 20 | VIEW_SEARCH_RESULT = 'view_search_results', 21 | VIEW_SEARCH_RESULTS = 'view_search_results', 22 | } 23 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/initializers/google-analytics-router.initializer.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentRef, Injector } from '@angular/core'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { NavigationEnd, NavigationStart, Router } from '@angular/router'; 4 | import { Subject } from 'rxjs'; 5 | import { GoogleAnalyticsService } from '../services/google-analytics.service'; 6 | import { GoogleAnalyticsRouterInitializer } from './google-analytics-router.initializer'; 7 | 8 | describe('googleAnalyticsRouterInitializer(settings, gaService)', () => { 9 | 10 | function fakeTransform(obj: Partial): Dest { 11 | return obj as any; 12 | } 13 | 14 | let gaService: GoogleAnalyticsService, 15 | spyOnGaService: jasmine.Spy, 16 | router$: Subject, 17 | router: Router, 18 | component: ComponentRef; 19 | 20 | beforeEach(() => { 21 | gaService = TestBed.inject(GoogleAnalyticsService); 22 | spyOnGaService = spyOn(gaService, 'pageView'); 23 | router$ = new Subject(); 24 | router = fakeTransform({ 25 | events: router$ 26 | }); 27 | component = fakeTransform>({ 28 | injector: fakeTransform({ 29 | get: () => router 30 | }), 31 | onDestroy: () => {} 32 | }); 33 | }); 34 | 35 | it('should not trigger the first route event', async () => { 36 | const factory = await GoogleAnalyticsRouterInitializer(null, gaService)(component); 37 | 38 | // act 39 | router$.next(new NavigationStart(1, '/test')); 40 | router$.next(new NavigationEnd(1, '/test', '/test')); 41 | router$.next(new NavigationEnd(1, '/test', '/test')); 42 | 43 | // asserts 44 | expect(spyOnGaService).toHaveBeenCalledTimes(1); 45 | }); 46 | 47 | it('should trigger the second route event', async () => { 48 | const factory = await GoogleAnalyticsRouterInitializer(null, gaService)(component); 49 | 50 | // act 51 | router$.next(new NavigationStart(1, '/test')); 52 | router$.next(new NavigationEnd(1, '/test', '/test')); 53 | router$.next(new NavigationEnd(1, '/test', '/test')); 54 | 55 | // asserts 56 | expect(spyOnGaService).toHaveBeenCalledTimes(1); 57 | expect(spyOnGaService).toHaveBeenCalledWith('/test', undefined); 58 | }); 59 | 60 | it('should trigger only included route', async () => { 61 | const factory = await GoogleAnalyticsRouterInitializer({ include: [ '/test' ] }, gaService)(component); 62 | 63 | // act 64 | router$.next(new NavigationStart(1, '/test')); 65 | router$.next(new NavigationEnd(1, '/test', '/test')); 66 | router$.next(new NavigationEnd(1, '/test', '/test')); 67 | router$.next(new NavigationStart(1, '/test1')); 68 | router$.next(new NavigationEnd(1, '/test1', '/test1')); 69 | router$.next(new NavigationStart(1, '/test2')); 70 | router$.next(new NavigationEnd(1, '/test2', '/test2')); 71 | 72 | // asserts 73 | expect(spyOnGaService).toHaveBeenCalledTimes(1); 74 | expect(spyOnGaService).toHaveBeenCalledWith('/test', undefined); 75 | }); 76 | 77 | it('should not trigger excluded route', async () => { 78 | const factory = await GoogleAnalyticsRouterInitializer({ exclude: [ '/test' ] }, gaService)(component); 79 | 80 | // act 81 | router$.next(new NavigationStart(1, '/test1')); 82 | router$.next(new NavigationEnd(1, '/test1', '/test1')); 83 | router$.next(new NavigationStart(1, '/test2')); 84 | router$.next(new NavigationEnd(1, '/test2', '/test2')); 85 | router$.next(new NavigationStart(1, '/test')); 86 | router$.next(new NavigationEnd(1, '/test', '/test')); 87 | router$.next(new NavigationEnd(1, '/test', '/test')); 88 | 89 | // asserts 90 | expect(spyOnGaService).toHaveBeenCalledTimes(1); 91 | expect(spyOnGaService).toHaveBeenCalledWith('/test2', undefined); 92 | }); 93 | 94 | it('should work w/ include and exclude router', async () => { 95 | const factory = await GoogleAnalyticsRouterInitializer({ 96 | include: [ '/test*' ], 97 | exclude: [ '/test-2' ] }, 98 | gaService 99 | )(component); 100 | 101 | // act 102 | router$.next(new NavigationStart(1, '/test-1')); 103 | router$.next(new NavigationEnd(1, '/test-1', '/test-1')); 104 | router$.next(new NavigationEnd(1, '/test-1', '/test-1')); 105 | router$.next(new NavigationStart(1, '/test-2')); 106 | router$.next(new NavigationEnd(1, '/test-2', '/test-2')); 107 | 108 | // asserts 109 | expect(spyOnGaService).toHaveBeenCalledTimes(1); 110 | expect(spyOnGaService).toHaveBeenCalledWith('/test-1', undefined); 111 | }); 112 | 113 | it('should match simple uri', async () => { 114 | const factory = await GoogleAnalyticsRouterInitializer({ 115 | include: [ '/test-1' ] }, 116 | gaService 117 | )(component); 118 | 119 | // act 120 | router$.next(new NavigationStart(1, '/test-1')); 121 | router$.next(new NavigationEnd(1, '/test-1', '/test-1')); 122 | router$.next(new NavigationEnd(1, '/test-1', '/test-1')); 123 | 124 | // asserts 125 | expect(spyOnGaService).toHaveBeenCalledTimes(1); 126 | expect(spyOnGaService).toHaveBeenCalledWith('/test-1', undefined); 127 | }); 128 | 129 | it('should match wildcard uri', async () => { 130 | const factory = await GoogleAnalyticsRouterInitializer( 131 | { 132 | include: [ '/test*' ] 133 | }, 134 | gaService 135 | )(component); 136 | 137 | // act 138 | router$.next(new NavigationStart(1, '/test-1')); 139 | router$.next(new NavigationEnd(1, '/test-1', '/test-1')); 140 | router$.next(new NavigationEnd(1, '/test-1', '/test-1')); 141 | 142 | // asserts 143 | expect(spyOnGaService).toHaveBeenCalledTimes(1); 144 | expect(spyOnGaService).toHaveBeenCalledWith('/test-1', undefined); 145 | }); 146 | 147 | it('should match RegExp uri', async () => { 148 | const factory = await GoogleAnalyticsRouterInitializer({ 149 | include: [ new RegExp('/test.*', 'i') ] }, 150 | gaService 151 | )(component); 152 | 153 | // act 154 | router$.next(new NavigationStart(1, '/test-1')); 155 | router$.next(new NavigationEnd(1, '/test-1', '/test-1')); 156 | router$.next(new NavigationEnd(1, '/test-1', '/test-1')); 157 | 158 | // asserts 159 | expect(spyOnGaService).toHaveBeenCalledTimes(1); 160 | expect(spyOnGaService).toHaveBeenCalledWith('/test-1', undefined); 161 | }); 162 | 163 | }); 164 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/initializers/google-analytics-router.initializer.ts: -------------------------------------------------------------------------------- 1 | import { Provider, APP_BOOTSTRAP_LISTENER, ComponentRef } from '@angular/core'; 2 | import { Router, NavigationEnd } from '@angular/router'; 3 | import { IGoogleAnalyticsRoutingSettings } from '../interfaces/i-google-analytics-routing-settings'; 4 | import { GoogleAnalyticsService } from '../services/google-analytics.service'; 5 | import { NGX_GOOGLE_ANALYTICS_ROUTING_SETTINGS_TOKEN } from '../tokens/ngx-google-analytics-router-settings-token'; 6 | import { filter, skip } from 'rxjs/operators'; 7 | 8 | /** 9 | * Provide a DI Configuration to attach GA Trigger to Router Events at Angular Startup Cycle. 10 | */ 11 | export const NGX_GOOGLE_ANALYTICS_ROUTER_INITIALIZER_PROVIDER: Provider = { 12 | provide: APP_BOOTSTRAP_LISTENER, 13 | multi: true, 14 | useFactory: GoogleAnalyticsRouterInitializer, 15 | deps: [ 16 | NGX_GOOGLE_ANALYTICS_ROUTING_SETTINGS_TOKEN, 17 | GoogleAnalyticsService 18 | ] 19 | }; 20 | 21 | /** 22 | * Attach a listener to `NavigationEnd` Router event. So, every time Router finish the page resolution it should call `NavigationEnd` event. 23 | * We assume that NavigationEnd is the final page resolution and call GA `page_view` command. 24 | * 25 | * To avoid double binds, we also destroy the subscription when de Bootstrap Component is destroied. But, we don't know for sure 26 | * that this strategy does not cause double bind on multiple bootstrap components. 27 | * 28 | * We are using de component's injector reference to resolve Router, sou I hope there is no problem w/ double bing. 29 | * 30 | * If you have this problem, I encourage not Use NgxGoogleAnalyticsRouterModule and atach the listener on AppComponent initialization. 31 | */ 32 | export function GoogleAnalyticsRouterInitializer( 33 | settings: IGoogleAnalyticsRoutingSettings, 34 | gaService: GoogleAnalyticsService 35 | ) { 36 | return async (c: ComponentRef) => { 37 | const router = c.injector.get(Router); 38 | const { include = [], exclude = [] } = settings ?? {}; 39 | const includeRules = normalizePathRules(include); 40 | const excludeRules = normalizePathRules(exclude); 41 | const subs = router 42 | .events 43 | .pipe( 44 | filter((event: any) => event instanceof NavigationEnd), 45 | skip(1), // Prevend double views on the first tigger (because GA Already send one ping on setup) 46 | filter(event => includeRules.length > 0 47 | ? includeRules.some(rule => rule.test(event.urlAfterRedirects)) 48 | : true), 49 | filter(event => excludeRules.length > 0 50 | ? !excludeRules.some(rule => rule.test(event.urlAfterRedirects)) 51 | : true) 52 | ) 53 | .subscribe(event => gaService.pageView(event.urlAfterRedirects, undefined)); 54 | // Cleanup 55 | c.onDestroy(() => subs.unsubscribe()); 56 | }; 57 | } 58 | 59 | /** Converts all path rules from string to Regex instances */ 60 | function normalizePathRules(rules: Array): Array { 61 | return rules.map(rule => (rule instanceof RegExp) 62 | ? rule 63 | : new RegExp(`^${rule.replace('*', '.*')}$`, 'i')); 64 | } 65 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/initializers/google-analytics.initializer.ts: -------------------------------------------------------------------------------- 1 | import { Provider, APP_INITIALIZER, isDevMode } from '@angular/core'; 2 | import { NGX_GOOGLE_ANALYTICS_SETTINGS_TOKEN } from '../tokens/ngx-google-analytics-settings-token'; 3 | import { IGoogleAnalyticsSettings } from '../interfaces/i-google-analytics-settings'; 4 | import { NGX_GTAG_FN } from '../tokens/ngx-gtag-token'; 5 | import { GtagFn } from '../types/gtag.type'; 6 | import { DOCUMENT } from '@angular/common'; 7 | 8 | /** 9 | * Provide a DI Configuration to attach GA Initialization at Angular Startup Cycle. 10 | */ 11 | export const NGX_GOOGLE_ANALYTICS_INITIALIZER_PROVIDER: Provider = { 12 | provide: APP_INITIALIZER, 13 | multi: true, 14 | useFactory: GoogleAnalyticsInitializer, 15 | deps: [ 16 | NGX_GOOGLE_ANALYTICS_SETTINGS_TOKEN, 17 | NGX_GTAG_FN, 18 | DOCUMENT 19 | ] 20 | }; 21 | 22 | /** 23 | * Create a script element on DOM and link it to Google Analytics tracking code URI. 24 | * After that, execute exactly same init process as tracking snippet code. 25 | */ 26 | export function GoogleAnalyticsInitializer( 27 | settings: IGoogleAnalyticsSettings, 28 | gtag: GtagFn, 29 | document: Document 30 | ) { 31 | return async () => { 32 | if (!settings.trackingCode) { 33 | if (!isDevMode()) { 34 | console.error('Empty tracking code for Google Analytics. Make sure to provide one when initializing NgxGoogleAnalyticsModule.'); 35 | } 36 | 37 | return; 38 | } 39 | 40 | if (!gtag) { 41 | if (!isDevMode()) { 42 | console.error('Was not possible create or read gtag() fn. Make sure this module is running on a Browser w/ access to Window interface.'); 43 | } 44 | 45 | return; 46 | } 47 | 48 | if (!document) { 49 | if (!isDevMode()) { 50 | console.error('Was not possible to access Document interface. Make sure this module is running on a Browser w/ access do Document interface.'); 51 | } 52 | } 53 | 54 | // Set default ga.js uri 55 | settings.uri = settings.uri || `https://www.googletagmanager.com/gtag/js?id=${settings.trackingCode}`; 56 | 57 | // these commands should run first! 58 | settings.initCommands = settings?.initCommands ?? []; 59 | 60 | // assert config command 61 | if (!settings.initCommands.find(x => x.command === 'config')) { 62 | settings.initCommands.unshift({ command: 'config', values: [ settings.trackingCode ] }); 63 | } 64 | 65 | // assert js command 66 | if (!settings.initCommands.find(x => x.command === 'js')) { 67 | settings.initCommands.unshift({ command: 'js', values: [ new Date() ] }); 68 | } 69 | 70 | for (const command of settings.initCommands) { 71 | gtag(command.command, ...command.values); 72 | } 73 | 74 | const s: HTMLScriptElement = document.createElement('script'); 75 | s.async = true; 76 | s.src = settings.uri; 77 | 78 | if (settings.nonce) { 79 | s.setAttribute('nonce', settings.nonce); 80 | } 81 | 82 | const head: HTMLHeadElement = document.getElementsByTagName('head')[0]; 83 | head.appendChild(s); 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/interfaces/i-google-analytics-command.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Standardizes a common command protocol :) 3 | */ 4 | export interface IGoogleAnalyticsCommand { 5 | command: string; 6 | values: Array; 7 | } 8 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/interfaces/i-google-analytics-routing-settings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Provide some custom settings for Automatics Router listener behaviour. 3 | */ 4 | export interface IGoogleAnalyticsRoutingSettings { 5 | /** 6 | * Exclude the given path to the auto page-view trigger. 7 | * 8 | * ```ts 9 | * @NgModule({ 10 | * imports: [ 11 | * NgxGoogleAnalyticsModule.forRoot(...), 12 | * NgxGoogleAnalyticsRouterModule.forRoot({ exclude: ['/login', '/internal/*', /regExp/gi] }) 13 | * ] 14 | * }) 15 | * AppModule 16 | * ``` 17 | */ 18 | exclude?: Array; 19 | 20 | /** 21 | * Auto trigger page-view only for allowed uris. 22 | * 23 | * ```ts 24 | * @NgModule({ 25 | * imports: [ 26 | * NgxGoogleAnalyticsModule.forRoot(...), 27 | * NgxGoogleAnalyticsRouterModule.forRoot({ include: ['/login', '/internal/*', /regExp/gi] }) 28 | * ] 29 | * }) 30 | * AppModule 31 | * ``` 32 | */ 33 | include?: Array; 34 | } 35 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/interfaces/i-google-analytics-settings.ts: -------------------------------------------------------------------------------- 1 | import { IGoogleAnalyticsCommand } from './i-google-analytics-command'; 2 | 3 | /** 4 | * Standardize an key-value objet to configure GA installation. 5 | */ 6 | export interface IGoogleAnalyticsSettings { 7 | /** Is mandatory to provide a tracking code folks... */ 8 | trackingCode: string; 9 | /** You can inject custom initialization commands like UserId or other e-commerce features. */ 10 | initCommands?: Array; 11 | /** If Google changes the uri and I die, you can survive! */ 12 | uri?: string; 13 | /** If true, trace GA tracking errors in production mode */ 14 | enableTracing?: boolean; 15 | /** If has a value, nonce will be added to script tag **/ 16 | nonce?: string; 17 | } 18 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/ngx-google-analytics-router/ngx-google-analytics-router.module.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders, NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { NGX_GOOGLE_ANALYTICS_ROUTER_INITIALIZER_PROVIDER } from '../initializers/google-analytics-router.initializer'; 4 | import { NgxGoogleAnalyticsModule } from '../ngx-google-analytics.module'; 5 | import { IGoogleAnalyticsRoutingSettings } from '../interfaces/i-google-analytics-routing-settings'; 6 | import { NGX_GOOGLE_ANALYTICS_ROUTING_SETTINGS_TOKEN } from '../tokens/ngx-google-analytics-router-settings-token'; 7 | 8 | /** 9 | * Attach a listener to `NavigationEnd` Router event. So, every time Router finish the page resolution it should call `NavigationEnd` event. 10 | * We assume that NavigationEnd is the final page resolution and call GA `page_view` command. 11 | * 12 | * To avoid double binds, we also destroy the subscription when de Bootstrap Component is destroied. But, we don't know for sure 13 | * that this strategy does not cause double bind on multiple bootstrap components. 14 | * 15 | * We are using de component's injector reference to resolve Router, sou I hope there is no problem w/ double bing. 16 | * 17 | * If you have this problem, I encourage not Use NgxGoogleAnalyticsRouterModule and atach the listener on AppComponent initialization. 18 | * 19 | * This Module is just a sugar for: 20 | * 21 | ```typescript 22 | constructor(private router: Router) {} 23 | ... 24 | ngOnInit() { 25 | ... 26 | this.router 27 | .events 28 | .pipe(takeUntil(this.onDestroy$)) 29 | .subscribe(event => { 30 | if (event instanceof NavigationEnd) { 31 | gaService.pageView(event.urlAfterRedirects, undefined); 32 | } 33 | }); 34 | ``` 35 | */ 36 | @NgModule({ 37 | imports: [ 38 | CommonModule, 39 | NgxGoogleAnalyticsModule 40 | ], 41 | providers: [ 42 | NGX_GOOGLE_ANALYTICS_ROUTER_INITIALIZER_PROVIDER 43 | ], 44 | declarations: [] 45 | }) 46 | export class NgxGoogleAnalyticsRouterModule { 47 | static forRoot(settings?: IGoogleAnalyticsRoutingSettings): ModuleWithProviders { 48 | return { 49 | ngModule: NgxGoogleAnalyticsRouterModule, 50 | providers: [ 51 | { 52 | provide: NGX_GOOGLE_ANALYTICS_ROUTING_SETTINGS_TOKEN, 53 | useValue: settings ?? {} 54 | } 55 | ] 56 | }; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/ngx-google-analytics.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from '@angular/core'; 2 | import { IGoogleAnalyticsCommand } from './interfaces/i-google-analytics-command'; 3 | import { NGX_GOOGLE_ANALYTICS_INITIALIZER_PROVIDER } from './initializers/google-analytics.initializer'; 4 | import { NGX_GOOGLE_ANALYTICS_SETTINGS_TOKEN } from './tokens/ngx-google-analytics-settings-token'; 5 | import { GaEventDirective } from './directives/ga-event.directive'; 6 | import { GaEventCategoryDirective } from './directives/ga-event-category.directive'; 7 | import { GaEventFormInputDirective } from './directives/ga-event-form-input.directive'; 8 | import { IGoogleAnalyticsSettings } from './interfaces/i-google-analytics-settings'; 9 | 10 | /** 11 | * Install Google Analytics Tracking code on your environment and configure tracking ID. 12 | * 13 | * This module should be a dependency on the highest level module of the application, i.e. AppModule in most use cases. 14 | */ 15 | @NgModule({ 16 | imports: [ 17 | ], 18 | declarations: [ 19 | GaEventDirective, 20 | GaEventCategoryDirective, 21 | GaEventFormInputDirective 22 | ], 23 | exports: [ 24 | GaEventDirective, 25 | GaEventCategoryDirective, 26 | GaEventFormInputDirective 27 | ] 28 | }) 29 | export class NgxGoogleAnalyticsModule { 30 | /** 31 | * You should provide a valid Google TrackingCode. This code will be provided to the entire application by 32 | * `NGX_GOOGLE_ANALYTICS_SETTINGS_TOKEN` token. You can inject this code in you components if you like by 33 | * use the following injection code `@Inject(NGX_GOOGLE_ANALYTICS_SETTINGS_TOKEN) gaConfig: IGoogleAnalyticsSettings` 34 | * 35 | * @param trackingCode The Google Tracking Code 36 | * @param initCommands When placed, it will run any GA Commands in sequence after setup GA environment. 37 | * @param uri When placed, it will change the default js URI to the provided one. 38 | * @param enableTracing When true, trace GA tracking errors on production mode. 39 | * @param nonce When placed, nonce will be added to script tag. 40 | */ 41 | static forRoot(trackingCode: string, initCommands: IGoogleAnalyticsCommand[] = [], uri?: string, enableTracing?: boolean, nonce?: string): ModuleWithProviders { 42 | return { 43 | ngModule: NgxGoogleAnalyticsModule, 44 | providers: [ 45 | { 46 | provide: NGX_GOOGLE_ANALYTICS_SETTINGS_TOKEN, 47 | useValue: { 48 | trackingCode, 49 | initCommands, 50 | uri, 51 | enableTracing, 52 | nonce 53 | } as IGoogleAnalyticsSettings 54 | }, 55 | NGX_GOOGLE_ANALYTICS_INITIALIZER_PROVIDER, 56 | ] 57 | }; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/services/google-analytics.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { NgxGoogleAnalyticsModule } from '../ngx-google-analytics.module'; 3 | import { GoogleAnalyticsService } from './google-analytics.service'; 4 | 5 | describe('GoogleAnalyticsService', () => { 6 | 7 | window['dataLayer'] = []; 8 | 9 | window['gtag'] = function () { 10 | window['dataLayer'].push(arguments as any); 11 | }; 12 | 13 | const tracking = 'GA-000000000'; 14 | let spyOnConsole: jasmine.Spy, 15 | spyOnGtag: jasmine.Spy; 16 | 17 | beforeEach(() => { 18 | TestBed.configureTestingModule({ 19 | imports: [ 20 | NgxGoogleAnalyticsModule.forRoot(tracking) 21 | ] 22 | }); 23 | }); 24 | 25 | beforeEach(() => { 26 | spyOnConsole = spyOn(console, 'error'); 27 | spyOnGtag = spyOn(window as any, 'gtag'); 28 | }); 29 | 30 | it('should call gtag fn w/ action/command pair', () => { 31 | const action = 'action', 32 | command = 'command', 33 | ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService); 34 | // act 35 | ga.gtag(action, command); 36 | // specs 37 | expect(spyOnGtag).toHaveBeenCalledWith(action, command); 38 | }); 39 | 40 | describe('gtag(`event`)', () => { 41 | 42 | it('should call a `event` action on gtag command', () => { 43 | const action = 'video_auto_play_start', 44 | ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService); 45 | 46 | ga.event(action); 47 | 48 | expect(spyOnGtag).toHaveBeenCalledWith('event', action); 49 | }); 50 | 51 | it('should find `event_category` property on gtag command', () => { 52 | const action = 'video_auto_play_start', 53 | event_category = 'video_auto_play', 54 | ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService); 55 | 56 | ga.event(action, event_category); 57 | 58 | expect(spyOnGtag).toHaveBeenCalledWith('event', action, { event_category }); 59 | }); 60 | 61 | it('should find `event_label` property on gtag command', () => { 62 | const action = 'video_auto_play_start', 63 | event_label = 'My promotional video', 64 | ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService); 65 | 66 | ga.event(action, undefined, event_label); 67 | 68 | expect(spyOnGtag).toHaveBeenCalledWith('event', action, { event_label }); 69 | }); 70 | 71 | it('should find `value` property on gtag command', () => { 72 | const action = 'video_auto_play_start', 73 | value = 40, 74 | ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService); 75 | 76 | ga.event(action, undefined, undefined, value); 77 | 78 | expect(spyOnGtag).toHaveBeenCalledWith('event', action, { value }); 79 | }); 80 | 81 | it('should find `interaction` property on gtag command', () => { 82 | const action = 'video_auto_play_start', 83 | interaction = true, 84 | ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService); 85 | 86 | ga.event(action, undefined, undefined, undefined, interaction); 87 | 88 | expect(spyOnGtag).toHaveBeenCalledWith('event', action, { interaction }); 89 | }); 90 | 91 | }); 92 | 93 | describe('gtag(`config`) aka pageView', () => { 94 | 95 | it('should call a `config` action on gtag command', () => { 96 | const page_path = '/page.html', 97 | ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService); 98 | 99 | ga.pageView(page_path); 100 | 101 | expect(spyOnGtag).toHaveBeenCalledWith('config', tracking, { page_path, page_location: document.location.href }); 102 | }); 103 | 104 | it('should send `page_title` attribute on gtag command', () => { 105 | const page_path = '/page.html', 106 | page_title = 'My Page View', 107 | ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService); 108 | 109 | ga.pageView(page_path, page_title); 110 | 111 | expect(spyOnGtag).toHaveBeenCalledWith('config', tracking, { page_path, page_title, page_location: document.location.href }); 112 | }); 113 | 114 | it('should send `page_location` attribute on gtag command', () => { 115 | const page_path = '/page.html', 116 | page_location = 'my location', 117 | ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService); 118 | 119 | ga.pageView(page_path, undefined, page_location); 120 | 121 | expect(spyOnGtag).toHaveBeenCalledWith('config', tracking, { page_path, page_location }); 122 | }); 123 | 124 | it('should use `document.location.href` as a default `page_location`', () => { 125 | const page_path = '/page.html', 126 | ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService); 127 | 128 | ga.pageView(page_path, undefined); 129 | 130 | expect(spyOnGtag).toHaveBeenCalledWith('config', tracking, { page_path, page_location: document.location.href }); 131 | }); 132 | 133 | }); 134 | 135 | describe('gtag(`event`)', () => { 136 | 137 | it('should call a `event` action on gtag command', () => { 138 | const screen_name = 'Home Screen', 139 | app_name = 'My App', 140 | ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService); 141 | 142 | ga.appView(screen_name, app_name); 143 | 144 | expect(spyOnGtag).toHaveBeenCalledWith('event', 'screen_view', { screen_name, app_name }); 145 | }); 146 | 147 | it('should send `app_id` property on gtag command', () => { 148 | const screen_name = 'Home Screen', 149 | app_name = 'My App', 150 | app_id = '2333', 151 | ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService); 152 | 153 | ga.appView(screen_name, app_name, app_id); 154 | 155 | expect(spyOnGtag).toHaveBeenCalledWith('event', 'screen_view', { screen_name, app_name, app_id }); 156 | }); 157 | 158 | it('should send `app_version` property on gtag command', () => { 159 | const screen_name = 'Home Screen', 160 | app_name = 'My App', 161 | app_version = 'v1.0', 162 | ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService); 163 | 164 | ga.appView(screen_name, app_name, undefined, app_version); 165 | 166 | expect(spyOnGtag).toHaveBeenCalledWith('event', 'screen_view', { screen_name, app_name, app_version }); 167 | }); 168 | 169 | it('should send `app_installer_id` property on gtag command', () => { 170 | const screen_name = 'Home Screen', 171 | app_name = 'My App', 172 | app_installer_id = '30000', 173 | ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService); 174 | 175 | ga.appView(screen_name, app_name, undefined, undefined, app_installer_id); 176 | 177 | expect(spyOnGtag).toHaveBeenCalledWith('event', 'screen_view', { screen_name, app_name, app_installer_id }); 178 | }); 179 | 180 | }); 181 | 182 | 183 | describe('gtag(`event`, `exception`)', () => { 184 | 185 | it('should call `event` action w/ `exception type on gtag command`', () => { 186 | const ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService); 187 | 188 | ga.exception(); 189 | 190 | expect(spyOnGtag).toHaveBeenCalledWith('event', 'exception'); 191 | }); 192 | 193 | it('should send `description` attribute on gtag command', () => { 194 | const description = 'Deu muito ruim', 195 | ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService); 196 | 197 | ga.exception(description); 198 | 199 | expect(spyOnGtag).toHaveBeenCalledWith('event', 'exception', { description }); 200 | }); 201 | 202 | it('should send `fatal` attribute on gtag command', () => { 203 | const fatal = true, 204 | ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService); 205 | 206 | ga.exception(undefined, fatal); 207 | 208 | expect(spyOnGtag).toHaveBeenCalledWith('event', 'exception', { fatal }); 209 | }); 210 | 211 | }); 212 | 213 | describe('gtag(`set`, ...)', () => { 214 | 215 | it('should send `set` command on gtag() call', () => { 216 | const setData = { currency: 'USD', country: 'US' }, 217 | ga: GoogleAnalyticsService = TestBed.inject(GoogleAnalyticsService); 218 | 219 | ga.set(setData); 220 | 221 | expect(spyOnGtag).toHaveBeenCalledWith('set', setData); 222 | }); 223 | }); 224 | 225 | }); 226 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/services/google-analytics.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject, isDevMode } from '@angular/core'; 2 | import { NGX_GOOGLE_ANALYTICS_SETTINGS_TOKEN } from '../tokens/ngx-google-analytics-settings-token'; 3 | import { IGoogleAnalyticsSettings } from '../interfaces/i-google-analytics-settings'; 4 | import { GaActionEnum } from '../enums/ga-action.enum'; 5 | import { DOCUMENT } from '@angular/common'; 6 | import { NGX_GTAG_FN } from '../tokens/ngx-gtag-token'; 7 | import { GtagFn } from '../types/gtag.type'; 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class GoogleAnalyticsService { 13 | 14 | private get document(): Document { 15 | return this._document; 16 | } 17 | 18 | constructor( 19 | @Inject(NGX_GOOGLE_ANALYTICS_SETTINGS_TOKEN) private readonly settings: IGoogleAnalyticsSettings, 20 | @Inject(DOCUMENT) private readonly _document: any, 21 | @Inject(NGX_GTAG_FN) private readonly _gtag: GtagFn 22 | ) { } 23 | 24 | private throw(err: Error) { 25 | if ((this.settings.enableTracing || isDevMode()) && console && console.error) { 26 | console.error(err); 27 | } 28 | } 29 | 30 | /** @todo Change this to `Object.fromEntity()` in the future... */ 31 | private toKeyValue(map: Map): { [param: string]: any } | void { 32 | return (map.size > 0) 33 | ? Array.from(map).reduce( 34 | (obj, [key, value]) => Object.defineProperty(obj, key, { value, enumerable: true }), 35 | {} 36 | ) 37 | : undefined; 38 | } 39 | 40 | /** 41 | * Call native GA Tag 42 | */ 43 | gtag(...args: any[]) { 44 | try { 45 | this._gtag(...args.filter(x => x !== undefined)); 46 | } catch (err: any) { 47 | this.throw(err); 48 | } 49 | } 50 | 51 | /** 52 | * Send an event trigger to GA. It is the same as call: 53 | * ```js 54 | * gtag('event', 'video_auto_play_start', { 55 | * 'event_label': 'My promotional video', 56 | * 'event_category': 'video_auto_play' 57 | * }); 58 | * ``` 59 | * 60 | * @param action 'video_auto_play_start' 61 | * @param category 'video_auto_play' 62 | * @param label 'My promotional video' 63 | * @param value An value to measure something 64 | * @param interaction If user interaction is performed 65 | */ 66 | event(action: GaActionEnum | string, category?: string, label?: string, value?: number, interaction?: boolean, options?: Object) { 67 | try { 68 | const opt = new Map(); 69 | if (category) { 70 | opt.set('event_category', category); 71 | } 72 | if (label) { 73 | opt.set('event_label', label); 74 | } 75 | if (value) { 76 | opt.set('value', value); 77 | } 78 | if (interaction !== undefined) { 79 | opt.set('interaction', interaction); 80 | } 81 | if (options) { 82 | Object 83 | .entries(options) 84 | .map(([key, value]) => opt.set(key, value)); 85 | } 86 | const params = this.toKeyValue(opt); 87 | if (params) { 88 | this.gtag('event', action as string, params); 89 | } else { 90 | this.gtag('event', action as string); 91 | } 92 | } catch (error: any) { 93 | this.throw(error); 94 | } 95 | } 96 | 97 | /** 98 | * Send an page view event. This is the same as 99 | * 100 | * ```js 101 | * gtag('config', 'GA_TRACKING_ID', { 102 | * 'page_title' : 'Homepage', 103 | * 'page_path': '/home' 104 | * }); 105 | * ``` 106 | * 107 | * The tracking ID is injected automatically by Inject Token NGX_GOOGLE_ANALYTICS_SETTINGS_TOKEN 108 | * 109 | * @param path /home 110 | * @param title Homepage 111 | * @param location '{ page_location }' 112 | * @param options '{ ... custom dimentions }' 113 | */ 114 | pageView( path: string, title?: string, location?: string, options?: Object) { 115 | try { 116 | const opt = new Map([['page_path', path]]); 117 | if (title) { 118 | opt.set('page_title', title); 119 | } 120 | if (location || this.document) { 121 | opt.set('page_location', (location || this.document.location.href)); 122 | } 123 | if (options) { 124 | Object 125 | .entries(options) 126 | .map(([key, value]) => opt.set(key, value)); 127 | } 128 | this.gtag('config', this.settings.trackingCode, this.toKeyValue(opt)); 129 | } catch (error: any) { 130 | this.throw(error); 131 | } 132 | } 133 | 134 | /** 135 | * Send an event to report a App Page View. It is the same as 136 | * 137 | * ```js 138 | * gtag('event', 'screen_view', { 139 | * 'app_name': 'myAppName', 140 | * 'screen_name' : 'Home' 141 | * }); 142 | * 143 | * ``` 144 | * 145 | * @param screen 'screen_name' 146 | * @param appName 'app_name' 147 | * @param appId 'app_id' 148 | * @param appVersion 'app_version' 149 | * @param installerId 'app_installer_id' 150 | */ 151 | appView(screen: string, appName: string, appId?: string, appVersion?: string, installerId?: string) { 152 | try { 153 | const opt = new Map([['screen_name', screen], ['app_name', appName]]); 154 | if (appId) { 155 | opt.set('app_id', appId); 156 | } 157 | if (appVersion) { 158 | opt.set('app_version', appVersion); 159 | } 160 | if (installerId) { 161 | opt.set('app_installer_id', installerId); 162 | } 163 | this.gtag('event', 'screen_view', this.toKeyValue(opt)); 164 | } catch (error: any) { 165 | this.throw(error); 166 | } 167 | } 168 | 169 | /** 170 | * Defines persistent values on GoogleAnalytics 171 | * 172 | * @see https://developers.google.com/analytics/devguides/collection/gtagjs/setting-values 173 | * 174 | * ```js 175 | * gtag('set', { 176 | * 'currency': 'USD', 177 | * 'country': 'US' 178 | * }); 179 | * ``` 180 | */ 181 | set(...options: Array) { 182 | try { 183 | this._gtag('set', ...options); 184 | } catch (err: any) { 185 | this.throw(err); 186 | } 187 | } 188 | 189 | /** 190 | * Send an event to GA to report an application error. It is the same as 191 | * 192 | * ```js 193 | * gtag('event', 'exception', { 194 | * 'description': 'error_description', 195 | * 'fatal': false // set to true if the error is fatal 196 | * }); 197 | * ``` 198 | * 199 | * @param description 'error_description' 200 | * @param fatal set to true if the error is fatal 201 | */ 202 | exception(description?: string, fatal?: boolean) { 203 | try { 204 | const opt = new Map(); 205 | if (description) { 206 | opt.set('description', description); 207 | } 208 | if (fatal) { 209 | opt.set('fatal', fatal); 210 | } 211 | const params = this.toKeyValue(opt); 212 | if (params) { 213 | this.gtag('event', 'exception', this.toKeyValue(opt)); 214 | } else { 215 | this.gtag('event', 'exception'); 216 | } 217 | } catch (error: any) { 218 | this.throw(error); 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/tokens/ngx-data-layer-token.ts: -------------------------------------------------------------------------------- 1 | import { DataLayer } from '../types/data-layer.type'; 2 | import { InjectionToken, inject } from '@angular/core'; 3 | import { NGX_WINDOW } from './ngx-window-token'; 4 | import { GaWindow } from './ngx-google-analytics-window'; 5 | 6 | /** 7 | * Check if there is some global function called gtag on Window object, or create an empty function to doesn't brake codes... 8 | */ 9 | export function getDataLayerFn(window: GaWindow): DataLayer { 10 | return (window) 11 | ? window['dataLayer'] = window['dataLayer'] || [] 12 | : null; 13 | } 14 | 15 | /** 16 | * Provides an injection token to access Google Analytics DataLayer Collection 17 | */ 18 | export const NGX_DATA_LAYER = new InjectionToken('ngx-data-layer', { 19 | providedIn: 'root', 20 | factory: () => getDataLayerFn(inject(NGX_WINDOW)) 21 | }); 22 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/tokens/ngx-google-analytics-router-settings-token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import { IGoogleAnalyticsRoutingSettings } from '../interfaces/i-google-analytics-routing-settings'; 3 | 4 | /** 5 | * Provide a Injection Token to global settings. 6 | */ 7 | export const NGX_GOOGLE_ANALYTICS_ROUTING_SETTINGS_TOKEN = new InjectionToken('ngx-google-analytics-routing-settings', { 8 | factory: () => ({}) 9 | }); 10 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/tokens/ngx-google-analytics-settings-token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import { IGoogleAnalyticsSettings } from '../interfaces/i-google-analytics-settings'; 3 | 4 | /** 5 | * Provide a Injection Token to global settings. 6 | */ 7 | export const NGX_GOOGLE_ANALYTICS_SETTINGS_TOKEN = new InjectionToken('ngx-google-analytics-settings', { 8 | factory: () => ({ trackingCode: '', enableTracing: false }) 9 | }); 10 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/tokens/ngx-google-analytics-window.ts: -------------------------------------------------------------------------------- 1 | export type GaWindow = Window & { 2 | gtag?: any; 3 | dataLayer?: any; 4 | } -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/tokens/ngx-gtag-token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken, inject } from '@angular/core'; 2 | import { NGX_WINDOW } from './ngx-window-token'; 3 | import { GtagFn } from '../types/gtag.type'; 4 | import { DataLayer } from '../types/data-layer.type'; 5 | import { NGX_DATA_LAYER } from './ngx-data-layer-token'; 6 | import { GaWindow } from './ngx-google-analytics-window'; 7 | 8 | /** 9 | * Check if there is some global function called gtag on Window object, or create an empty function to doesn't brake codes... 10 | */ 11 | export function getGtagFn(window: GaWindow, dataLayer: DataLayer): GtagFn { 12 | return (window) 13 | ? window['gtag'] = window['gtag'] || function () { 14 | dataLayer.push(arguments as any); 15 | } 16 | : null; 17 | } 18 | 19 | /** 20 | * Provides an injection token to access Google Analytics Gtag Function 21 | */ 22 | export const NGX_GTAG_FN = new InjectionToken('ngx-gtag-fn', { 23 | providedIn: 'root', 24 | factory: () => getGtagFn(inject(NGX_WINDOW), inject(NGX_DATA_LAYER)) 25 | }); 26 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/tokens/ngx-window-token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken, inject } from '@angular/core'; 2 | import { DOCUMENT } from '@angular/common'; 3 | import { GaWindow } from './ngx-google-analytics-window'; 4 | 5 | /** 6 | * Provide DOM Window reference. 7 | */ 8 | export const NGX_WINDOW = new InjectionToken('ngx-window', { 9 | providedIn: 'root', 10 | factory: () => { 11 | const { defaultView } = inject(DOCUMENT); 12 | 13 | if (!defaultView) { 14 | throw new Error('Window is not available'); 15 | } 16 | 17 | return defaultView; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/types/data-layer.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides an interface os a GA command list. 3 | */ 4 | export type DataLayer = Array<(string | { [param: string]: string })>; 5 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/types/ga-action.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A string that represents a default GA action used by Google to generate e-commerce inteligence. 3 | * 4 | * You can provide a custom string as well. 5 | * @deprecated use lib/enums/ga-action.enum.ts instead 6 | */ 7 | export type GaAction = 'view_search_results' 8 | | 'add_payment_info' 9 | | 'add_to_cart' 10 | | 'add_to_wishlist' 11 | | 'begin_checkout' 12 | | 'checkout_progress' 13 | | 'generate_lead' 14 | | 'login' 15 | | 'purchase' 16 | | 'refund' 17 | | 'remove_from_cart' 18 | | 'search' 19 | | 'select_content' 20 | | 'set_checkout_option' 21 | | 'share' 22 | | 'sign_up' 23 | | 'view_item' 24 | | 'view_item_list' 25 | | 'view_promotion'; 26 | 27 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/lib/types/gtag.type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Google Analytics GTagFn call signature 3 | */ 4 | export type GtagFn = (...args: (string | { [param: string]: string })[]) => {}; 5 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/public_api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ngx-google-analytics 3 | */ 4 | 5 | export * from './lib/directives/ga-event-category.directive'; 6 | export * from './lib/directives/ga-event.directive'; 7 | export * from './lib/directives/ga-event-form-input.directive'; 8 | 9 | export * from './lib/enums/ga-action.enum'; 10 | 11 | export * from './lib/initializers/google-analytics.initializer'; 12 | export * from './lib/initializers/google-analytics-router.initializer'; 13 | 14 | export * from './lib/interfaces/i-google-analytics-command'; 15 | export * from './lib/interfaces/i-google-analytics-routing-settings'; 16 | export * from './lib/interfaces/i-google-analytics-settings'; 17 | 18 | export * from './lib/services/google-analytics.service'; 19 | 20 | export * from './lib/tokens/ngx-data-layer-token'; 21 | export * from './lib/tokens/ngx-google-analytics-router-settings-token'; 22 | export * from './lib/tokens/ngx-google-analytics-settings-token'; 23 | export * from './lib/tokens/ngx-gtag-token'; 24 | export * from './lib/tokens/ngx-window-token'; 25 | 26 | export * from './lib/types/data-layer.type'; 27 | export * from './lib/types/ga-action.type'; 28 | export * from './lib/types/gtag.type'; 29 | 30 | export * from './lib/ngx-google-analytics.module'; 31 | export * from './lib/ngx-google-analytics-router/ngx-google-analytics-router.module'; 32 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js'; 4 | import 'zone.js/testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | declare const require: any; 12 | 13 | // First, initialize the Angular testing environment. 14 | getTestBed().initTestEnvironment( 15 | BrowserDynamicTestingModule, 16 | platformBrowserDynamicTesting(), { 17 | teardown: { destroyAfterEach: false } 18 | } 19 | ); 20 | // Then we find all the tests. 21 | const context = require.context('./', true, /\.spec\.ts$/); 22 | // And load the modules. 23 | context.keys().map(context); 24 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "declarationMap": true, 6 | "target": "es2015", 7 | "module": "es2015", 8 | "moduleResolution": "node", 9 | "declaration": true, 10 | "sourceMap": true, 11 | "inlineSources": true, 12 | "experimentalDecorators": true, 13 | "importHelpers": true, 14 | "types": [], 15 | "lib": [ 16 | "dom", 17 | "es2017" 18 | ] 19 | }, 20 | "angularCompilerOptions": { 21 | "skipTemplateCodegen": true, 22 | "strictMetadataEmit": true, 23 | "fullTemplateTypeCheck": true, 24 | "strictInjectionParameters": true, 25 | "enableResourceInlining": true 26 | }, 27 | "exclude": [ 28 | "src/test.ts", 29 | "**/*.spec.ts" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /projects/ngx-google-analytics/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "compilationMode": "partial" 8 | } 9 | } -------------------------------------------------------------------------------- /projects/ngx-google-analytics/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { RouterModule, Routes } from '@angular/router'; 2 | import { NgModule } from '@angular/core'; 3 | import { TestPageAComponent } from './test-page-a/test-page-a.component'; 4 | import { TestPageBComponent } from './test-page-b/test-page-b.component'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: 'page-1', 9 | component: TestPageAComponent 10 | }, 11 | { 12 | path: 'page-2', 13 | component: TestPageBComponent 14 | } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [ 19 | RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' }) 20 | ], 21 | exports: [ 22 | RouterModule 23 | ] 24 | }) 25 | export class AppRoutingModule { } 26 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxandriani/ngx-google-analytics/2dc6bab54ef0a2104e39977e9385291427edf60f/src/app/app.component.css -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

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

6 | 7 | 13 | 14 | 15 | 16 |
17 |

Directive tests

18 | 19 |
20 | 21 | 22 | 23 |
24 | 25 |
26 | 27 |
28 | 29 |

Group Directive Test

30 | 31 |
32 | 33 | 34 | 35 |
-------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(waitForAsync(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [ 8 | AppComponent 9 | ], 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | 19 | it(`should have as title 'ngx-google-analytics-sdk'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.debugElement.componentInstance; 22 | expect(app.title).toEqual('ngx-google-analytics-sdk'); 23 | }); 24 | 25 | it('should render title in a h1 tag', () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.debugElement.nativeElement; 29 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to ngx-google-analytics-sdk!'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ga-app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent { 9 | title = 'ngx-google-analytics-sdk'; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AppComponent } from './app.component'; 5 | import { NgxGoogleAnalyticsModule, NgxGoogleAnalyticsRouterModule } from 'ngx-google-analytics'; 6 | import { environment } from 'src/environments/environment'; 7 | import { TestPageAComponent } from './test-page-a/test-page-a.component'; 8 | import { TestPageBComponent } from './test-page-b/test-page-b.component'; 9 | import { RouterModule } from '@angular/router'; 10 | import { AppRoutingModule } from './app-routing.module'; 11 | 12 | @NgModule({ 13 | declarations: [ 14 | AppComponent, 15 | TestPageAComponent, 16 | TestPageBComponent 17 | ], 18 | imports: [ 19 | BrowserModule, 20 | AppRoutingModule, 21 | RouterModule, 22 | NgxGoogleAnalyticsModule.forRoot(environment.ga), 23 | NgxGoogleAnalyticsRouterModule.forRoot({ include: [ '/page-*' ] }), 24 | ], 25 | bootstrap: [AppComponent] 26 | }) 27 | export class AppModule { } 28 | -------------------------------------------------------------------------------- /src/app/test-page-a/test-page-a.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxandriani/ngx-google-analytics/2dc6bab54ef0a2104e39977e9385291427edf60f/src/app/test-page-a/test-page-a.component.css -------------------------------------------------------------------------------- /src/app/test-page-a/test-page-a.component.html: -------------------------------------------------------------------------------- 1 |

2 | test-page-a works! 3 |

4 | 5 | -------------------------------------------------------------------------------- /src/app/test-page-a/test-page-a.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { TestPageAComponent } from './test-page-a.component'; 4 | 5 | describe('TestPageAComponent', () => { 6 | let component: TestPageAComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ TestPageAComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(TestPageAComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/test-page-a/test-page-a.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ga-app-test-page-a', 5 | templateUrl: './test-page-a.component.html', 6 | styleUrls: ['./test-page-a.component.css'] 7 | }) 8 | export class TestPageAComponent { 9 | } 10 | -------------------------------------------------------------------------------- /src/app/test-page-b/test-page-b.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxandriani/ngx-google-analytics/2dc6bab54ef0a2104e39977e9385291427edf60f/src/app/test-page-b/test-page-b.component.css -------------------------------------------------------------------------------- /src/app/test-page-b/test-page-b.component.html: -------------------------------------------------------------------------------- 1 |

2 | test-page-b works! 3 |

4 | 5 | -------------------------------------------------------------------------------- /src/app/test-page-b/test-page-b.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { TestPageBComponent } from './test-page-b.component'; 4 | 5 | describe('TestPageBComponent', () => { 6 | let component: TestPageBComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ TestPageBComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(TestPageBComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/test-page-b/test-page-b.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ga-app-test-page-b', 5 | templateUrl: './test-page-b.component.html', 6 | styleUrls: ['./test-page-b.component.css'] 7 | }) 8 | export class TestPageBComponent { 9 | } 10 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxandriani/ngx-google-analytics/2dc6bab54ef0a2104e39977e9385291427edf60f/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | ga: 'tracking-code' 4 | }; 5 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | ga: 'UA-17886362-3' 8 | }; 9 | 10 | /* 11 | * For easier debugging in development mode, you can import the following file 12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 13 | * 14 | * This import should be commented out in production mode because it will have a negative impact 15 | * on performance if an error is thrown. 16 | */ 17 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 18 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxandriani/ngx-google-analytics/2dc6bab54ef0a2104e39977e9385291427edf60f/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgxGoogleAnalyticsSdk 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/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'), 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 | }); 31 | }; -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | 14 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | /** 7 | 8 | * By default, zone.js will patch all possible macroTask and DomEvents 9 | 10 | * user can disable parts of macroTask/DomEvents patch by setting following flags 11 | 12 | */ 13 | 14 | 15 | 16 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 17 | 18 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 19 | 20 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 21 | 22 | 23 | 24 | /* 25 | 26 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 27 | 28 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 29 | 30 | */ 31 | 32 | // (window as any).__Zone_enable_cross_context_check = true; 33 | 34 | 35 | 36 | /*************************************************************************************************** 37 | 38 | * Zone JS is required by default for Angular itself. 39 | 40 | */ 41 | 42 | import 'zone.js'; // Included with Angular CLI. 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | /*************************************************************************************************** 51 | 52 | * APPLICATION IMPORTS 53 | 54 | */ 55 | 56 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 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 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting(), { 16 | teardown: { destroyAfterEach: false } 17 | } 18 | ); 19 | // Then we find all the tests. 20 | const context = require.context('./', true, /\.spec\.ts$/); 21 | // And load the modules. 22 | context.keys().map(context); 23 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "main.ts", 9 | "polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "downlevelIteration": true, 6 | "importHelpers": true, 7 | "outDir": "./dist/out-tsc", 8 | "sourceMap": true, 9 | "declaration": false, 10 | "module": "es2020", 11 | "moduleResolution": "node", 12 | "experimentalDecorators": true, 13 | "target": "es2015", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2017", 19 | "dom" 20 | ], 21 | "paths": { 22 | "ngx-google-analytics": [ 23 | "dist/ngx-google-analytics" 24 | ], 25 | "ngx-google-analytics/*": [ 26 | "dist/ngx-google-analytics/*" 27 | ] 28 | } 29 | } 30 | } 31 | --------------------------------------------------------------------------------