├── .babelrc ├── .gitignore ├── .jshintrc ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README-GTM-CONFIG.md ├── README-GTM-REFERENCE.md ├── README.md ├── build └── gtm │ └── main.js ├── core └── modules │ ├── closest.js │ ├── cookie.js │ ├── delegate.js │ ├── find.js │ ├── flatten.js │ ├── getkey.js │ ├── has.js │ ├── hasClass.js │ ├── init.js │ ├── internalMap.js │ ├── log.js │ ├── matches.js │ ├── merge.js │ ├── on.js │ ├── reduceBool.js │ ├── sanitize.js │ └── text.js ├── documentation-images ├── event_name.png ├── once_per_page.png ├── tag_dataquality.png ├── tag_event.png ├── tag_pageview.png ├── tag_timing.png └── var_gasettings.png ├── gtm ├── expose.js ├── globalVars.js ├── main.js └── modules │ ├── dataLayer.js │ ├── event.js │ ├── localHelperFactory.js │ ├── pageview.js │ ├── safeFn.js │ └── timing.js ├── gulpfile.js ├── package-lock.json ├── package.json └── test ├── core └── modules │ ├── closet.test.js │ ├── cookie.test.js │ ├── has.test.js │ ├── sanitize.test.js │ └── text.test.js ├── index.test.js ├── jsdom-site-mock.js └── src └── site └── index.html /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/env" 4 | ], 5 | "env": { 6 | "test": { 7 | "plugins": [ 8 | "istanbul" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Coverage directory used by tools like istanbul 13 | coverage 14 | 15 | # Dependency directories 16 | node_modules 17 | .idea 18 | .nyc_output 19 | .scannerwork 20 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "browser": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "esnext": true, 7 | "immed": true, 8 | "indent": 3, 9 | "jasmine": true, 10 | "latedef": true, 11 | "newcap": false, 12 | "noarg": true, 13 | "node": true, 14 | "regexp": true, 15 | "smarttabs": true, 16 | "strict": true, 17 | "trailing": true, 18 | "undef": true, 19 | "unused": true, 20 | "globals": { 21 | "alert": true, 22 | "angular": true, 23 | "assert": true, 24 | "confirm": true, 25 | "dataLayer": true, 26 | "EventEmitter": true, 27 | "expect": true, 28 | "Promise": true, 29 | "should": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "endOfLine": "crlf", 4 | "useTabs": false 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | 4 | node_js: 5 | - 8.11.2 6 | 7 | addons: 8 | sonarcloud: 9 | organization: default 10 | token: 11 | secure: "" 12 | 13 | install: 14 | - npm install 15 | 16 | before_script: 17 | - npm install -g gulp 18 | 19 | script: 20 | npm test 21 | gulp 22 | 23 | after_success: npm run coverage 24 | 25 | deploy: 26 | provider: script 27 | skip_cleanup: true 28 | script: 29 | - npx semantic-release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 DP6 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-GTM-CONFIG.md: -------------------------------------------------------------------------------- 1 | # Configuração do GTM 2 | 3 | Este documento descreve os passos para a utilização da biblioteca analytics-helper em conjunto com o Google Tag Manager, e as configurações necessárias. 4 | 5 | ## Tag principal 6 | 7 | O arquivo final, presente na pasta _build_, seja ele o arquivo de exemplo disponível neste repositório, ou uma versão personalizada gerada via Gulp, deverá ser copiado integralmente para uma Tag Custom HTML. 8 | 9 | Em configurações avançadas, a opção de executar uma única vez por página deverá ser selecionada. 10 | 11 | ![Configuração uma por página](documentation-images/once_per_page.png) 12 | 13 | As tags que utilizarem o objeto analyticsHelper devem configurar esta tag principal como requisito na seção _sequência de tags_, para garantir que o objeto estará definido antes do uso. 14 | 15 | ## Acionadores 16 | 17 | Quatro acionadores do tipo _evento personalizado_ devem ser criados. 18 | 19 | Os nomes dos eventos serão os mesmos nomes utilizados para identificá-los na camada de dados: _gtm_dataQuality_event_, _ga_pageview_, _ga_event_ e _ga_timing_. 20 | 21 | ![Triggers](documentation-images/event_name.png) 22 | 23 | ## Tags de template 24 | 25 | Quatro tags de Universal Analytics devem ser criadas, uma para cada acionador criado acima. 26 | 27 | Estas tags devem ser preenchidas com as variáveis de camada de dados listadas na próxima seção. Estes são os campos padrões, as tags podem ser modificadas para incluir ou remover dimensões personalizadas, dados de ecommerce e outras configurações. 28 | 29 | ### Template de Pageview (+ GA Settings) 30 | 31 | ![Tag de template de Pageview](documentation-images/tag_pageview.png) 32 | 33 | ### Template de Evento (+ GA Settings) 34 | 35 | ![Tag de template de Evento](documentation-images/tag_event.png) 36 | 37 | ### Template de Timing (+ GA Settings) 38 | 39 | ![Tag de template de Timing](documentation-images/tag_timing.png) 40 | 41 | ### Template de Data Quality 42 | 43 | ![Tag de template de DataQuality](documentation-images/tag_dataquality.png) 44 | 45 | ### Template de GA Settings 46 | 47 | ![Tag de template de GA Settings](documentation-images/var_gasettings.png) 48 | 49 | ## Variáveis 50 | 51 | As variáveis padrão _Container ID_ e _Debug Mode_ devem ser habilitadas, pois elas são utilizadas pelo código da _tag principal_. 52 | 53 | A tabela a seguir descreve todas as variáveis do tipo _variável de camada de dados_ que deverão ser criadas para o uso nas tags de template do Google Analytics: 54 | 55 | | Nome da variável de Camada de Dados | Tag que utiliza | Campo do template | 56 | | ----------------------------------- | ------------------- | ----------------------------- | 57 | | eventCategory | Tag de Evento | Categoria | 58 | | eventAction | Tag de Evento | Ação | 59 | | eventLabel | Tag de Evento | Rótulo | 60 | | eventValue | Tag de Evento | Valor | 61 | | eventNoInteraction | Tag de Evento | Hit de não-interação | 62 | | timingCategory | Tag de Evento | Categoria | 63 | | timingVariable | Tag de Evento | Variável | 64 | | timingValue | Tag de Evento | Valor | 65 | | timingLabel | Tag de Evento | Rótulo | 66 | | path | GA Settings | Fields to Set -> page | 67 | | userId | GA Settings | Fields to Set -> userId | 68 | | dataQuality.category | Tag de Data Quality | Categoria | 69 | | dataQuality.action | Tag de Data Quality | Ação | 70 | | dataQuality.label | Tag de Data Quality | Rótulo | 71 | | dataQuality.selector | Tag de Data Quality | Dimensões Personalizadas -> 1 | 72 | | dataQuality.event | Tag de Data Quality | Dimensões Personalizadas -> 2 | 73 | 74 | Caso utilize a opção waitQueue (habilitada por padrão), a seguinte variável de _javascript personalizado_ deverá ser criada e adicionada ao campo _hitCallback_ em _Fields to Set_ nas tags de template de Google Analytics. 75 | 76 | ```javascript 77 | function () { 78 | return function () { 79 | analyticsHelper.internal.sentPageview = true; 80 | while (analyticsHelper.internal.eventQueue.length) { 81 | analyticsHelper.event.apply(analyticsHelper, analyticsHelper.internal.eventQueue.shift()); 82 | } 83 | while (analyticsHelper.internal.timingQueue.length) { 84 | analyticsHelper.timing.apply(analyticsHelper, analyticsHelper.internal.timingQueue.shift()); 85 | } 86 | }; 87 | } 88 | ``` 89 | -------------------------------------------------------------------------------- /README-GTM-REFERENCE.md: -------------------------------------------------------------------------------- 1 | # Referência técnica 2 | 3 | > Analytics Helper para o Google Tag Manager 4 | 5 | Este documento introduz as APIs e funcionalidades desenvolvidas para o suporte ao Google Tag Manager (GTM). São importantes algumas configurações do lado da própria ferramenta para que o código implementado na tag do Analytics Helper tenha o comportamento esperado. Mais detalhes sobre [aqui](https://github.com/DP6/analytics-helper/blob/master/README-GTM-CONFIG.md). 6 | 7 | ## Objeto options 8 | 9 | O objeto `options` contém as configurações globais do _Analytics Helper_. Os valores padrões servem na maioria dos casos, por isso devem ser alterados com cautela e de forma consciente. 10 | 11 | ```javascript 12 | var options = { 13 | helperName: 'analyticsHelper', 14 | dataLayerName: 'dataLayer', 15 | debug: ({{Debug Mode}} || false), 16 | waitQueue: true, 17 | containerId: ({{Container ID}} || ''), 18 | exceptionEvent: 'gtm_dataQuality_event', 19 | exceptionCategory: 'GTM Exception', 20 | customNamePageview: 'ga_pageview', 21 | customNameEvent: 'ga_event', 22 | customNameTiming: 'ga_timing', 23 | errorSampleRate: 1 24 | }; 25 | ``` 26 | 27 | ### init(opt_options) 28 | 29 | Utilize esta função, de caráter opcional, para inicializar o Analytics Helper com opções diferentes das padrões. Recebe como argumento o objeto `opt_options`, que possui as seguintes chaves: 30 | 31 | - `helperName` -- Por padrão `"analyticsHelper"`. 32 | Uma string que indentifica o nome da instância do _Analytics Helper_ no objeto _window_ do navegator. O tagueamento não é afetado pela mudança desse valor, se feito pela função `safeFn` (recomendado). 33 | 34 | - `dataLayerName` -- Por padrão `"dataLayer"`. 35 | Uma string que identifica o nome da instância da _camada de dados_ no objeto _window_ do navegador. Deve ser o mesmo valor configurado no _snippet_ do GTM para que as funções de interface do _Analytics Helper_ (ex: `getDataLayer`) funcionem. 36 | 37 | - `debug` -- Por padrão é a variavél `{{Debug Mode}}` do GTM. Se desabilitada, é `false`. 38 | Um booleano que sinaliza para o _Analytics Helper_ se o contexto atual é de depuração ou produção. Caso verdadeiro, os eventos serão disparados apenas via `console.log`, sem envios para o GA. 39 | 40 | - `waitQueue` -- Por padrão é `true` 41 | Um booleano que sinaliza para o _Analytics Helper_ se ele deve utilizar uma fila de espera nos eventos. Caso verdadeiro, todos eventos serão empilhados numa estrutura interna até que ocorra o primeiro pageview na página. Recomendamos que essa opção esteja sempre ativada, pois evita inconsistências nos relatórios do Google Analytics. 42 | 43 | - `containerId` -- Por padrão é a variável `{{Container ID}}` do GTM. Se desabilitada, é a string vazia `''`. 44 | Uma string que deve ser equivalente ao ID do contêiner do GTM onde o _Analytics Helper_ foi configurado (GTM-XXXXX). 45 | 46 | - `exceptionEvent` -- Por padrão `"gtm_dataQuality_event"`. 47 | Uma string que identifica o evento enviado à camada de dados caso ocorra alguma exceção no código do GTM. Esta opção suporta a ideia da coleta para uma propriedade do Google Analytics de [_Quality Assurence_](https://www.observepoint.com/blog/why-automate-your-web-analytics-qa/) . Para entender melhor o uso desta configuração, [consultar documentação de configuração do GTM](https://github.com/DP6/analytics-helper/blob/master/README-GTM-CONFIG.md). 48 | 49 | - `exceptionCategory` -- Por padrão `"GTM Exception"`. 50 | Uma string que indica qual o valor que deve ser preenchido na chave `"event_category"` do evento enviado à camada de dados caso ocorra alguma exceção no código do GTM. Esta opção suporta a ideia da coleta para uma propriedade do Google Analytics de [_Quality Assurence_](https://www.observepoint.com/blog/why-automate-your-web-analytics-qa/) . Para entender melhor o uso desta configuração, [consultar documentação de configuração do GTM](https://github.com/DP6/analytics-helper/blob/master/README-GTM-CONFIG.md) 51 | 52 | - `customNamePageview` -- Por padrão `"ga_pageview"`. 53 | Uma string que identifica o evento enviado à camada de dados toda vez que a função `pageview` (ver abaixo) for chamada. 54 | 55 | - `customNameEvent` -- Por padrão `"ga_event"`. 56 | Uma string que identifica o evento enviado à camada de dados toda vez que a função `event` (ver abaixo) for chamada. 57 | 58 | - `customNameTiming` -- Por padrão `"ga_timing"`. 59 | Uma string que identifica o evento de timing enviado à camada de dados toda vez que a função `timing` (ver abaixo) for chamada. 60 | 61 | - `errorSampleRate` -- Por padrão `1` . 62 | Deve ser um inteiro entre 0 e 1, que controla o nível de amostragem dos erros enviados ao GA de _Data Quality_ **(mais detalhes à adicionar)**. Serve para controlar a coleta em ambientes onde o volume de disparos é muito grande. 63 | 64 | ## API 65 | 66 | ### Coleta Google Analytics (GA) 67 | 68 | As funções a seguir possuem especificidades para coleta de dados baseado nas ferramentas GA e GTM. Devido a isso, as funções internas desta API utilizam a variável criada pelo GTM chamada [`dataLayer`](https://developers.google.com/tag-manager/devguide). Para garantir que as funcionalidades das funções estejam corretas, será necessário garantir que o ambiente em questão possua a camada de dados inicializada corretamente. 69 | 70 | #### pageview(path, object) 71 | 72 | Utilizada para o disparo de pageview personalizado. 73 | 74 | ##### Parâmetros 75 | 76 | - `path` (opcional): String que recebe o path do Pageview personalizado. 77 | - `object` (opcional): Objeto que será atribuído ao pageview. Pode ser utilizado para passar objetos de Enhanced Ecommerce, além de métricas e dimensões personalizadas. Qualquer chave personalizada será inserida como push no dataLayer. 78 | 79 | ##### Exemplo de código 80 | 81 | ```javascript 82 | analyticsHelper.pageview('/post/finalizou-leitura', { 83 | area: 'Aberta', 84 | categoria: 'Data Science' 85 | }); 86 | ``` 87 | 88 | #### event(category, action, label, object) 89 | 90 | #### event(category, action, label, value, object) 91 | 92 | Utilizada para efetuar disparos de eventos. 93 | 94 | ##### Parâmetros 95 | 96 | - `category`: String que representa a categoria do evento. 97 | - `action`: String que representa a ação do evento. 98 | - `label` (opcional): String que pode representar o label do evento. 99 | - `object` (opcional): Objeto que será atribuído ao evento. Pode ser utilizado para passar objetos de Enhanced Ecommerce, além de métricas e dimensões personalizadas. Qualquer chave personalizada será inserida como push no dataLayer. 100 | 101 | _Importante_: A chave value pode ser passada tanto como o quarto valor da chamada quanto como um parâmetro do objeto `"object"`. 102 | 103 | ##### Exemplo de código 104 | 105 | ```javascript 106 | analyticsHelper.event('MinhaCategoria', 'MinhaAcao', 'MeuRotulo', 0, { 107 | cidade: 'São Paulo' 108 | }); 109 | ``` 110 | 111 | ```javascript 112 | analyticsHelper.event('MinhaCategoria', 'MinhaAcao', 'MeuRotulo', { 113 | eventValue: 0, 114 | cidade: 'São Paulo' 115 | }); 116 | ``` 117 | 118 | ### Utilidades 119 | 120 | #### getDataLayer(key) 121 | 122 | Retorna qualquer objeto contido no dataLayer exposto no ambiente. Esta função é um encapsulamento da [macro .get() do GTM](https://developers.google.com/tag-manager/api/v1/reference/accounts/containers/macros). 123 | 124 | ##### Argumentos 125 | 126 | - `key`: String que representa a chave do objeto a ser recuperado. 127 | 128 | ##### Retorno: 129 | 130 | - **ANY**: O valor recuperado do modelo de dados do GTM. 131 | 132 | ##### Exemplo de código 133 | 134 | ```javascript 135 | dataLayer.push({ 136 | meuObjeto: 'valor', 137 | meuOutroObjeto: 'outroValor' 138 | }); 139 | 140 | analyticsHelper.getDataLayer('meuObjeto'); // valor 141 | ``` 142 | 143 | #### getKey(key, opt_root) 144 | 145 | Encontra um objeto ou valor pela chave informada. Caso alguma das chaves em cadeia não existir, a função retorna undefined, evitando assim o lançamento de erros. 146 | 147 | ##### Argumentos 148 | 149 | - `key`: String que representa a chave do objeto a ser encontrado 150 | - `opt_root` (Opcional): Objeto que possui a chave a ser encontrada. Por padrão é `window`. 151 | 152 | ##### Retorno 153 | 154 | - **ANY**: O valor recuperado do modelo de dados da variável informada. 155 | 156 | ##### Exemplo de código 157 | 158 | ```javascript 159 | var objeto = { 160 | meuObjeto: { 161 | meuArray: [ 162 | { 163 | minhaChave: 'encontrei meu valor' 164 | } 165 | ] 166 | } 167 | }; 168 | 169 | analyticsHelper.getKey('objeto.meuObjeto.meuArray.0.minhaChave'); // encontrei meu valor 170 | analyticsHelper.getKey('meuObjeto.meuArray.0.minhaChave', objeto); // encontrei meu valor 171 | analyticsHelper.getKey('chaveNaoExistente.meuArray.0.minhaChave', objeto); // undefined 172 | ``` 173 | 174 | #### sanitize(text, opts) 175 | 176 | Retorna um texto sem caracteres especiais, acentuação, espaços ou letras maiúsculas (opcionalmente). 177 | 178 | ##### Argumentos 179 | 180 | - `text`: String a ser tratada 181 | - `opts` (opcional): Objeto com variáveis para configuração da função sanitize. 182 | _ `capitalized`: Define a forma com que a String será tratada. - true: Coloca a String como Camel Case; - false: Coloca a String como Snake Case. 183 | _ `spacer`: Define qual texto será utilizado como separador no lugar de `_`. 184 | 185 | ##### Retorno 186 | 187 | - **String**: O valor recebido por parâmetro e modificado pela função. 188 | 189 | ##### Exemplo de código 190 | 191 | ```javascript 192 | analyticsHelper.sanitize('Minha String Suja'); // minha_string_suja 193 | analyticsHelper.sanitize('Minha String Suja', { capitalized: true }); // MinhaStringSuja 194 | analyticsHelper.sanitize('Minha String Suja', { spacer: '-' }); // minha-string-suja 195 | analyticsHelper.sanitize('Minha String Suja', { 196 | capitalized: true, 197 | spacer: '-' 198 | }); // Minha-String-Suja 199 | ``` 200 | 201 | #### cookie(name, value, opts) 202 | 203 | Cria um cookie ou retorna seu valor baseado nos parâmetros recebidos na função. 204 | 205 | ##### Argumentos 206 | 207 | - `name`: String que representa o nome do cookie; 208 | - `value`: String que representa o valor do cookie; 209 | - `opts` (opcional): Objeto com variáveis para configuração da função cookie: 210 | - `exdays` (opcional): Numeric que representa a quantidade de dias para a expiração do cookie; 211 | - `domain`: (opcional): String que representa o domínio ao qual o cookie deve ser atribuído; 212 | - `path` (opcional): String que representa o path do site ao qual o cookie deve ser atribuído; 213 | 214 | ##### Retorno 215 | 216 | - **String**: Valor completo do cookie criado ou recuperado. 217 | 218 | ##### Exemplo de criação de cookie 219 | 220 | ```javascript 221 | analyticsHelper.cookie('meuCookie', 'meuValor', { 222 | exdays: 3, // Dias para expiração 223 | domain: '.meudominio.com.br', // Domínio que o cookie atribuído 224 | path: '/meu-path' // Path do cookie 225 | }); // meuCookie=meuValor; expires=Sun, 16 Oct 2016 19:18:17 GMT; domain=.meudominio.com.br; path=/meu-path 226 | ``` 227 | 228 | ##### Exemplo de recuperar valor de um cookie 229 | 230 | ```javascript 231 | analyticsHelper.cookie('meuCookie'); // meuValor 232 | ``` 233 | 234 | ### SafeFn 235 | 236 | Função segura do Analytics Helper. O principal conceito por trás da sua utilização é a garantia da não interferência da coleta de dados no comportamento natural do portal de sua utilização, evitando vazamento de logs e erros ao ambiente em questão. 237 | 238 | Para efetivar essa proposta, a função recebe um callback de parâmetro. Dentro do escopo deste callback, é possível receber um objeto de parâmetro com funções estendidas do helper, com o intuito de garantir o encapsulamento de funções sensíveis. Este objeto será representado daqui em diante como "Helper Interno" (mais detalhes na próxima seção). 239 | 240 | #### Argumentos da função 241 | 242 | - `id`: Deve receber o nome da tag (do GTM) em que o código em questão estiver contido. 243 | - `callback`: Função de callback que cria o escopo para o ambiente seguro do safeFn. Passa via parâmetro o Helper Interno para utilização. 244 | - `immediate` (Opcional): Variável booleana, que por default (**true**) executa a função de callback imediatamente. Caso **false**, o retorno da função será a própria função segura, que deverá ser executada manualmente quando necessário. 245 | 246 | #### Retorno 247 | 248 | - **Function** ou **undefined**: Caso o parâmetro `immediate` receba o valor true, o safeFn executa o callback e retorna undefined. Porém se o parâmetro `immediate` ter o valor false, o retorno é a própria função de callback para ser executada posteriormente. 249 | 250 | ##### Exemplo de código 251 | 252 | ```javascript 253 | analyticsHelper.safeFn('Nome da Tag do GTM', function(helper) { 254 | helper.event('MinhaCategoria', 'MinhaAcao', 'MeuRotulo', 'MeuValor', { 255 | dimension1: 'São Paulo' 256 | }); 257 | }); 258 | 259 | var fn = analyticsHelper.safefn( 260 | 'Nome da Tag do GTM', 261 | function(helper) { 262 | console.log(new Date()); 263 | }, 264 | { immediate: false } 265 | ); 266 | 267 | setTimeout(fn, 2000); 268 | ``` 269 | 270 | #### Lançamento de Exceptions 271 | 272 | A função `safeFn` tem um tratamento específico para as Exceptions que ocorrerem dentro do seu escopo seguro. Utilizando as variáveis de personalização do Helper options.debug, options.exceptionEvent, options.exceptionCategory e options.errorSampleRate, a função atribui valores ao dataLayer do GTM, que utilizará a configuração do GTM para o envio de eventos ao Google Analytics. Esta prática é baseada na concepção de [Quality Assurance](https://www.observepoint.com/blog/why-automate-your-web-analytics-qa/). 273 | 274 | ### Helper interno 275 | 276 | Objeto com funções internas passados via parâmetro no callback da função `safeFn`. 277 | 278 | #### on(event, selector, callback, parent) 279 | 280 | O método `on` serve para executar um callback ao executar algum evento em um elemento HTML específico. Em caso de não haver jQuery na página, ele se baseia na função querySelectorAll do javascript, e por conta disso, é preciso ficar atento a compatibilidade dos navegadores. Não é recomendado a utilização desta função em páginas que oferecem suporte a IE 7 ou inferior. 281 | 282 | A presença do quarto argumento, `parent`, transforma a funcionalidade do método `on` na do método [`delegate`](#delegateevent-selector-callback). 283 | 284 | #### Argumentos 285 | 286 | - `event`: String do evento que ira executar o callback, exemplos: 'mousedown', 'click', etc. 287 | [Saiba mais](https://mdn.mozilla.org/en-US/docs/Web/Events). 288 | 289 | - `selector`: String do Seletor CSS que irá buscar os elementos que executarão o callback no disparo do evento. 290 | [Saiba mais](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors). 291 | 292 | - `callback`: Função executada no disparo do evento suprido no parâmetro `event`. 293 | 294 | - `parent` (opcional): Elemento raíz a partir de onde o evento deverá ser ouvido. 295 | 296 | ##### Exemplo de código 297 | 298 | ```javascript 299 | analyticsHelper.safeFn('Nome da Tag', function(helper) { 300 | helper.on('mousedown', '#botaoX', function(helper) { 301 | helper.event('MinhaCategoria', 'MinhaAcao', 'MeuRotulo'); 302 | }); 303 | }); 304 | 305 | analyticsHelper.safeFn('Nome da Tag', function(helper) { 306 | helper.on( 307 | 'mousedown', 308 | '#botaoX', 309 | function(helper) { 310 | helper.event('MinhaCategoria', 'MinhaAcao', 'MeuRotulo'); 311 | }, 312 | '#caixaY' 313 | ); 314 | }); 315 | ``` 316 | 317 | #### delegate(event, selector, callback) 318 | 319 | O método `delegate` serve para executar um callback ao executar algum evento em um elemento HTML específico. Diferentemente do `on`, ele assume como padrão que o evento deverá ser atrelado ao `document.body` e não ao seletor passado no argumento `selector`, esperando por qualquer evento que ocorra em um elemento que case com o argumento `selector`. 320 | 321 | Este método é preferível contra o método `on` nos casos em que o elemento ainda não exista na página ou quando ele pode existir e deixar de existir dependendo da navegação do usuário, como opções de um menu suspenso ou uma lista de scroll infinito. 322 | 323 | #### Argumentos 324 | 325 | - `event`: String do evento que ira executar o callback, exemplos: 'mousedown', 'click', etc. 326 | [Saiba mais](https://mdn.mozilla.org/en-US/docs/Web/Events). 327 | 328 | - `selector`: String do Seletor CSS ao qual os elementos que acionarem o evento do `body` deverão ser comparados. 329 | [Saiba mais](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors). 330 | 331 | - `callback`: Função executada no disparo do evento suprido no parâmetro `event`. 332 | 333 | ##### Exemplo de código 334 | 335 | ```javascript 336 | analyticsHelper.safeFn('Nome da Tag', function(helper) { 337 | helper.delegate('mousedown', '#botaoX', function(helper) { 338 | helper.event('MinhaCategoria', 'MinhaAcao', 'MeuRotulo'); 339 | }); 340 | }); 341 | 342 | // Equivalente a 343 | analyticsHelper.safeFn('Nome da Tag', function(helper) { 344 | helper.on( 345 | 'mousedown', 346 | '#botaoX', 347 | function(helper) { 348 | helper.event('MinhaCategoria', 'MinhaAcao', 'MeuRotulo'); 349 | }, 350 | document.body 351 | ); 352 | }); 353 | ``` 354 | 355 | #### wrap(elm) 356 | 357 | A função `wrap` provê diversas funções facilitadoras para interações com o DOM no intuito de padronizar e compatibilizar a coleta de dados em ambientes sem o conceito de [camada de dados](https://blog.dp6.com.br/o-que-%C3%A9-a-camada-de-dados-ou-data-layer-80f37fa3429c). A motivação para a elaboração desta função é a não dependências de bibliotecas de mercado, como o jQuery, com o intuito de não depender da instalação das mesmas nos ambientes tagueados. Ao executar a função, um objeto com as funções facilitadoras será retornado. 358 | 359 | ##### Argumentos 360 | 361 | - `elm` String, elemento HTML ou Array de elementos HTML. 362 | - String: o texto é utilizado como seletor CSS, criando um encapsulamento com todos os elementos que cruzarem com o seletor. 363 | - Elemento HTML, NodeList ou array de Elementos HMTL: serão utilizados os elementos supridos como base para o encapsulamento. 364 | 365 | ##### Retorno 366 | 367 | - **Object**: Encapsulamento com funções facilitadoras. 368 | 369 | ##### Exemplos de código 370 | 371 | ```javascript 372 | // Apenas um elemento 373 | analyticsHelper.safeFn('Nome da Tag', function(helper) { 374 | helper.on('mousedown', '#botaoX', function() { 375 | var text = helper.wrap(this).text({ sanitize: true }); 376 | helper.event('Categoria', 'Ação', 'Label_' + text); 377 | }); 378 | }); 379 | 380 | // Múltiplos elementos 381 | analyticsHelper.safeFn('Nome da Tag', function(helper) { 382 | var urls = helper.wrap('a'); 383 | console.log(urls.nodes); // Array de nodes a. 384 | }); 385 | ``` 386 | 387 | ### Objeto Wrap 388 | 389 | Objeto gerado pela função wrap, inclui diversas funções que ajudam na manipulação do DOM. As funções facilitadoras contidas neste objeto tem como objetivo diminuir a verbosidade do código JavaScript e evitar o uso de bibliotecas dependentes dos ambientes tagueados. 390 | 391 | #### Atributo nodes 392 | 393 | Array de elementos HTML ou [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) que será a base das funções. 394 | 395 | #### hasClass(className, opts) 396 | 397 | Função que verifica se o elemento HTML tem a classe passada por parâmetro. 398 | 399 | ##### Argumentos 400 | 401 | - `className`: String do nome da classe a ser batida com o elemento. 402 | 403 | - `opts` (opcional): Objeto com variáveis para configuração da função hasClass. \* `toArray`: Caso o valor seja true, retorna o array de resultados relacionados à comparação. 404 | 405 | ##### Retorno 406 | 407 | - **Boolean** ou **Array de Boolean**: Caso o parâmetro `opts`seja informado com o atributo `toArray`recebendo o valor true, o retorno da função será o array o boolean de elementos encontrados. Caso somente o parâmetro `className` seja informado, a função retorno true ou false se encontrar ou não algum elemento com a classe especificada. 408 | 409 | ##### Exemplo de código 410 | 411 | ```javascript 412 | analyticsHelper.safeFn('Nome da Tag', function(helper) { 413 | helper.on('mousedown', '.button', function() { 414 | if (helper.wrap(this).hasClass('myClass')) { 415 | helper.event('MinhaCategoria', 'MinhaAcao', 'MeuRotulo'); 416 | } 417 | }); 418 | }); 419 | ``` 420 | 421 | #### log(type, message, object) 422 | 423 | Um wrapper ao redor do Console nativo. Criado para garantir que execute apenas durante Debug Mode e apenas se console[type] existir. 424 | 425 | ##### Argumentos 426 | 427 | - `type` Tipo de console a ser realizado. Pode ser qualquer tipo suportado pelo console: `log`, `warn`, `error`, `table`, `group`... 428 | 429 | - `message` Texto a ser enviado para o console. 430 | 431 | - `object` (opcional): Qualquer objeto com mais detalhes do que deve ser enviado para o método escolhido. 432 | 433 | ##### Retorno 434 | 435 | - **undefined**: Nenhum retorno é enviado ou deverá ser esperado após a execução desta função. 436 | 437 | ##### Exemplo de código 438 | 439 | ```javascript 440 | analyticsHelper.safeFn('Nome da Tag', function(helper) { 441 | helper.on('mousedown', '.button', function() { 442 | if (helper.wrap(this).hasClass('myClass')) { 443 | helper.event('MinhaCategoria', 'MinhaAcao', 'MeuRotulo'); 444 | } else { 445 | helper.log('log', 'Classe "myClass" não encontrada'); 446 | } 447 | }); 448 | }); 449 | ``` 450 | 451 | #### matches(selector, reduce) 452 | 453 | Função que verifica se o elemento HTML confere com o seletor. 454 | 455 | ##### Argumentos 456 | 457 | - `selector`String do seletor a ser batido com o elemento. 458 | 459 | - `opts` (opcional): Objeto com variáveis para configuração da função matches. \* `toArray`: Caso o valor seja true, retorna o array de resultados relacionados à comparação. 460 | 461 | ##### Retorno 462 | 463 | - **Boolean** ou **Array de Boolean**: Caso o parâmetro `opts`seja informado com o atributo `toArray`recebendo o valor true, o retorno da função será o array o boolean de elementos encontrados. Caso somente o parâmetro `selector` seja informado, a função retorno true ou false se encontrar ou não algum elemento com a classe especificada. 464 | 465 | ##### Exemplo de código 466 | 467 | ```javascript 468 | analyticsHelper.safeFn('Nome da Tag', function(helper) { 469 | helper.on('mousedown', '.button', function() { 470 | if (helper.wrap(this).matches('.myForm .button')) { 471 | helper.event('MinhaCategoria', 'MinhaAcao', 'MeuRotulo'); 472 | } 473 | }); 474 | }); 475 | ``` 476 | 477 | #### closest(selector) 478 | 479 | Para cada elemento no conjunto, obtenha o primeiro elemento que corresponde ao seletor, testando o próprio elemento e atravessando seus antepassados ​​na árvore [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model). 480 | 481 | ##### Argumentos 482 | 483 | - `selector`: String do seletor CSS que baterá com o elemento HTML. 484 | 485 | ##### Retorno 486 | 487 | - **Wrap**: Um encapsulamento com os elementos que bateram com o seletor informado. 488 | 489 | ##### Exemplo de código 490 | 491 | ```javascript 492 | analyticsHelper.safeFn('Nome da Tag', function(helper) { 493 | helper.on('mousedown', '.button', function() { 494 | var text = helper 495 | .wrap(this) 496 | .closest('div.parentDivWithText') 497 | .text({ sanitize: true, onlyFirst: true }); 498 | helper.event('MinhaCategoria', 'MinhaAcao', 'MeuRotulo' + text); 499 | }); 500 | }); 501 | ``` 502 | 503 | #### text(opt) 504 | 505 | Função que retorna o texto do elemento. 506 | 507 | ##### Argumentos 508 | 509 | - `opt`: Objeto com variáveis para configuração da função text. 510 | - `sanitize`: Caso booleano `true`, utilizará o sanitize com as opções padrão. Caso seja um objeto, repassará as opções escolhidas ao sanitize interno. 511 | - `onlyFirst`: Boolean que em caso de true retorna somente o texto direto do elemento e não de todos os seus filhos. 512 | - `onlyText`: Boolean que em caso de true retorna o texto concatenado ao invés de um array de Strings. 513 | 514 | ##### Retorno 515 | 516 | ##### Exemplo de código 517 | 518 | ```javascript 519 | analyticsHelper.safeFn('Nome da Tag', function(helper) { 520 | var text = helper 521 | .wrap('#myId') 522 | .text({ sanitize: true, onlyFirst: true, onlyText: true }); 523 | 524 | var text2 = helper 525 | .wrap('#myOtherId') 526 | .text({ sanitize: { spacer: '/', capitalized: false } }); 527 | helper.pageview('/' + text + '/' + text2); 528 | }); 529 | ``` 530 | 531 | #### find(sel) 532 | 533 | Função que retorna um objeto Wrap de todos os elementos que batem com o seletor. 534 | 535 | ##### Argumentos 536 | 537 | - `sel`: String do seletor CSS que baterá com o elemento HTML. 538 | 539 | ##### Retorno 540 | 541 | ##### Exemplo de código 542 | 543 | ```javascript 544 | analyticsHelper.safeFn('Nome da Tag', function(helper) { 545 | var text = helper 546 | .wrap('#myId') 547 | .find('.myClass') 548 | .text({ sanitize: true }); 549 | helper.pageview('/' + text); 550 | }); 551 | ``` 552 | 553 | #### map(func, params) 554 | 555 | Função que executa um código para cada elemento. Possui o mesmo comportamento da API [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map). 556 | 557 | ##### Argumentos 558 | 559 | - `func`: Função a ser executada, pode receber um parâmetro que será a referência do elemento iterado. 560 | 561 | - `params`: Array de parâmetros utilizados na função. 562 | 563 | #### Exemplo de código 564 | 565 | ```javascript 566 | analyticsHelper.safeFn('Nome da Tag', function(helper) { 567 | var sources = helper.wrap('img').map(function(elm) { 568 | return elm.src; 569 | }); 570 | console.log(sources); // Array com os valores do atributo src de cada elemento img. 571 | }); 572 | ``` 573 | 574 | #### Tamanho do Pacote 575 | 576 | | Compactação | Tamanho (KB) | 577 | | ------------------- | ------------ | 578 | | Sem compactação | 14.71 | 579 | | Minificado pelo GTM | 7.14 | 580 | | Com GZip | 2.72 | 581 | 582 | #### Créditos 583 | 584 | **DP6 Koopas !!!** 585 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Analytics Helper 2 | 3 | [![Build Status](https://travis-ci.org/DP6/analytics-helper.svg?branch=master)](https://travis-ci.org/DP6/analytics-helper) [![Coverage Status](https://coveralls.io/repos/github/DP6/analytics-helper/badge.svg?branch=master)](https://coveralls.io/github/DP6/analytics-helper?branch=master) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square)](https://github.com/semantic-release/semantic-release) 4 | 5 | 6 | O Analytics Helper tem como objetivo facilitar a implementação, a manutenção e a padronização de *tags* no contexto de *digital analytics*. 7 | 8 | Um exemplo dos esforços envolvidos no suporte à padronização está na implementação de funções similares às da biblioteca jQuery, comumente utilizada em projetos de tagueamento. Deste modo, mesmo na ausência desta, será possível garantir o padrão e qualidade da coleta dos dados (Consultar a tabela de compatibilidade). Caso da jQuery exista, o Analytics Helper simplesmente delega a execução para ela, ou seja, o código nos dois casos será o mesmo. 9 | 10 | Resumindo, o utilize o Analytics Helper para: 11 | 12 | - Ter acesso à funções de manipulação do DOM sem depender da jQuery; 13 | - Enviar automaticamente alertas e eventos para o Google Analytics indicando possíveis erros de JavaScript no tagueamento; 14 | - Funções simplificadas para coleta de dados; 15 | - Padronização do código das tags. 16 | 17 | ### Estendendo o Analytics Helper 18 | 19 | Um dos principais conceitos do Helper é a manutenção de sua API com o versionamento básico [SemVer](https://semver.org/). Para isso, recomendamos que a expansão de sua API para situações específicas, ou seja, utilizadas em projetos com particularidades que não irão se repetir, seja feita através do objeto `fn`. 20 | 21 | #### Objeto `fn` 22 | 23 | Se trata de uma variável global dentro do escopo do objeto Helper, visando agrupar as funções que não pertencem ao escopo atual do projeto. 24 | 25 | ```javascript 26 | analyticsHelper.fn.minhaFuncao = function(name) { 27 | console.log(name); 28 | }; 29 | analyticsHelper.fn.minhaFuncao('DP6'); // DP6 30 | ``` 31 | ### Compatibilidade 32 | 33 | O Analytics Helper depende da função nativa `querySelectorAll`. Os navegadores com suporte a essa funcionalidade são: 34 | 35 | | Chrome | Firefox | IE | Opera | Safari | 36 | | ------ | ------- | --- | ----- | ------ | 37 | | 1 | 3.5 | 8 | 10 | 3.2 | 38 | 39 | ## Tag Managers 40 | 41 | Inicialmente, na versão 1.0, a biblioteca dá suporte para o Google Tag Manager com envio de dados para o Google Analytics, mas está planejado o suporte para demais ferramentas do mercado também. 42 | 43 | ### Google Tag Manager 44 | 45 | - [Documento de Referência Técnica](https://github.com/DP6/analytics-helper/blob/master/README-GTM-REFERENCE.md) 46 | - [Configuração do GTM](https://github.com/DP6/analytics-helper/blob/master/README-GTM-CONFIG.md) 47 | -------------------------------------------------------------------------------- /build/gtm/main.js: -------------------------------------------------------------------------------- 1 | ; 2 | (function () { 3 | 'use strict'; 4 | var hasOwnProperty = Object.prototype.hasOwnProperty; 5 | var jQuery = window.jQuery; 6 | var fn = {}; 7 | 8 | var options = { 9 | helperName: 'analyticsHelper', 10 | dataLayerName: 'dataLayer', 11 | debug: ({{Debug Mode}} || false), 12 | waitQueue: true, 13 | containerId: ({{Container ID}} || ''), 14 | exceptionEvent: 'gtm_dataQuality_event', 15 | exceptionCategory: 'GTM Exception', 16 | customNamePageview: 'ga_pageview', 17 | customNameEvent: 'ga_event', 18 | customNameTiming: 'ga_timing', 19 | errorSampleRate: 1, 20 | gtmCleanup: function (gtmId) { 21 | helper.setDataLayer('ecommerce', undefined); 22 | helper.setDataLayer('noInteraction', undefined); 23 | } 24 | }; 25 | 26 | var internal = { 27 | sentPageview: false 28 | }; 29 | 30 | var helper = { 31 | internal: internal, 32 | init: init, 33 | pageview: pageview, 34 | event: event, 35 | timing: timing, 36 | sanitize: sanitize, 37 | getDataLayer: getDataLayer, 38 | setDataLayer: setDataLayer, 39 | cookie: cookie, 40 | getKey: getKey, 41 | safeFn: safeFn, 42 | fn: fn, 43 | options: options 44 | }; 45 | 46 | function closest(elm, seletor) { 47 | if ('closest' in elm) return elm.closest(seletor); 48 | if (typeof jQuery === 'function') return jQuery(elm).closest(seletor)[0]; 49 | 50 | var parent = elm.parentNode; 51 | 52 | while (parent != document) { 53 | if (matches(parent, seletor)) { 54 | return parent; 55 | } 56 | parent = parent.parentNode; 57 | } 58 | return undefined; 59 | } 60 | 61 | function getCookie(key) { 62 | key = '; ' + key + '='; 63 | var cookie = '; ' + document.cookie; 64 | var index = cookie.indexOf(key); 65 | var end; 66 | if (index === -1) { 67 | return ''; 68 | } 69 | cookie = cookie.substring(index + key.length); 70 | end = cookie.indexOf(';'); 71 | return window.unescape(end === -1 ? cookie : cookie.substring(0, end)); 72 | } 73 | 74 | function setCookie(name, value, opts) { 75 | var exdate, cookie; 76 | opts = opts || {}; 77 | 78 | cookie = name + '=' + window.escape(value); 79 | if (opts.exdays) { 80 | exdate = new Date(); 81 | exdate.setDate(exdate.getDate() + opts.exdays); 82 | cookie += '; expires=' + exdate.toUTCString(); 83 | } 84 | if (opts.domain) { 85 | cookie += '; domain=' + opts.domain; 86 | } 87 | cookie += '; path=' + (opts.path || '/'); 88 | return (document.cookie = cookie); 89 | } 90 | 91 | function cookie(name, value, opts) { 92 | if (typeof value === 'undefined') 93 | return getCookie(name); 94 | 95 | return setCookie(name, value, opts); 96 | } 97 | 98 | function delegate(id, event, selector, oldHandler, parent) { 99 | var method, elm, handler; 100 | if (typeof jQuery === 'function') { 101 | elm = jQuery(parent || document); 102 | handler = safeFn(id, oldHandler, { 103 | event: event, 104 | selector: selector, 105 | immediate: false 106 | }); 107 | if (typeof elm.on === 'function') { 108 | return elm.on(event, selector, handler); 109 | } else if (typeof elm.delegate === 'function') { 110 | return elm.delegate(selector, event, handler); 111 | } 112 | } 113 | 114 | if (typeof parent === 'string') { 115 | parent = document.querySelectorAll(parent); 116 | } 117 | 118 | if (typeof document.addEventListener === 'function') { 119 | method = 'addEventListener'; 120 | } else { 121 | method = 'attachEvent'; 122 | event = 'on' + event; 123 | } 124 | 125 | handler = function(e) { 126 | for ( 127 | var target = e.target; target && target !== this; target = target.parentNode 128 | ) { 129 | if (matches(target, selector)) { 130 | var handler = safeFn(id, oldHandler, { 131 | event: event, 132 | selector: selector, 133 | immediate: false 134 | }); 135 | handler.call(target, e); 136 | break; 137 | } 138 | } 139 | }; 140 | 141 | if (Object.prototype.toString.call(parent) === '[object NodeList]') { 142 | for (var parentIndex = 0; parentIndex <= parent.length - 1; parentIndex++) { 143 | (parent[parentIndex] || document)[method](event, handler, false); 144 | } 145 | } else { 146 | (parent || document)[method](event, handler, false); 147 | } 148 | } 149 | 150 | function find(element, selector) { 151 | return element.querySelectorAll(selector); 152 | } 153 | 154 | function flatten(arrs) { 155 | var currentArray, currentElement, i, j; 156 | var result = []; 157 | 158 | if (arrs.length === 1) return arrs[0]; 159 | 160 | while (arrs.length > 0) { 161 | currentArray = arrs.shift(); 162 | for (i = 0; currentArray.length > i; i++) { 163 | currentElement = currentArray[i]; 164 | j = 0; 165 | while (j < result.length && currentElement !== result[j]) { 166 | j += 1; 167 | } 168 | if (j === result.length) result.push(currentElement); 169 | } 170 | } 171 | 172 | return result; 173 | } 174 | 175 | function getKey(key, opt_root) { 176 | if (!key || typeof key !== 'string') return undefined; 177 | 178 | var result = opt_root || window; 179 | var splitKey = key.split('.'); 180 | 181 | for (var i = 0; i < splitKey.length && result != null; i++) { 182 | if (has(result, splitKey[i])) { 183 | result = result[splitKey[i]]; 184 | } else { 185 | return undefined; 186 | } 187 | } 188 | return result; 189 | } 190 | 191 | function has(obj, key) { 192 | return hasOwnProperty.call(obj, key); 193 | } 194 | 195 | function hasClass(e, className) { 196 | if ('classList' in e) return e.classList.contains(className); 197 | 198 | return new RegExp('\\b' + className + '\\b').test(e.className); 199 | } 200 | 201 | function init(opt_options) { 202 | options = merge(options, opt_options); 203 | expose(); 204 | } 205 | 206 | function internalMap(elms, func, exArgs) { 207 | var elm, args; 208 | var ret = []; 209 | for (var index = 0; index < elms.length; index++) { 210 | elm = elms[index]; 211 | if (elm instanceof HTMLElement === false) 212 | throw 'internalMap: Esperado elemento HTML'; 213 | args = [elm].concat(exArgs); 214 | ret.push(func.apply(null, args)); 215 | } 216 | return ret; 217 | } 218 | 219 | function log(type, info, obj) { 220 | if (options.debug && typeof getKey('console.' + type) === 'function') { 221 | console[type](info, obj); 222 | } 223 | } 224 | 225 | function matches(elm, seletor) { 226 | if ('matches' in elm) return elm.matches(seletor); 227 | if (typeof jQuery === 'function') return jQuery(elm).is(seletor); 228 | 229 | var elms = elm.parentNode.querySelectorAll(seletor); 230 | 231 | for (var i = 0; i < elms.length; i++) { 232 | if (elms[i] === elm) { 233 | return true; 234 | } 235 | } 236 | return false; 237 | } 238 | 239 | function merge(obj, obj2) { 240 | if (obj2) { 241 | for (var key in obj2) { 242 | if (has(obj2, key)) { 243 | obj[key] = obj2[key]; 244 | } 245 | } 246 | } 247 | return obj; 248 | } 249 | 250 | function on(id, event, selector, oldCallback, parent) { 251 | var i, array, elm, callback; 252 | 253 | if (parent) return delegate(id, event, selector, oldCallback, parent); 254 | 255 | callback = safeFn(id, oldCallback, { 256 | event: event, 257 | selector: selector, 258 | immediate: false 259 | }); 260 | 261 | if (typeof jQuery === 'function') { 262 | elm = jQuery(selector); 263 | 264 | if (typeof elm.on === 'function') { 265 | return elm.on(event, callback); 266 | } else if (typeof elm.bind === 'function') { 267 | return elm.bind(event, callback); 268 | } 269 | } 270 | 271 | if (typeof selector === 'string') { 272 | array = document.querySelectorAll(selector); 273 | } else if (typeof selector.length === 'undefined' || selector === window) { 274 | array = [selector]; 275 | } else { 276 | array = selector; 277 | } 278 | 279 | for (i = 0; i < array.length; i++) { 280 | elm = array[i]; 281 | 282 | if (typeof elm.addEventListener === 'function') { 283 | elm.addEventListener(event, callback); 284 | } else { 285 | elm.attachEvent('on' + event, callback); 286 | } 287 | } 288 | } 289 | 290 | function reduceBool(arr) { 291 | var i; 292 | for (i = 0; i < arr.length; i++) { 293 | if (arr[i]) return true; 294 | } 295 | return false; 296 | } 297 | 298 | function sanitize(str, opts) { 299 | var split, i, spacer; 300 | 301 | if (!str) return ''; 302 | opts = opts || {}; 303 | spacer = typeof opts.spacer === 'string' ? opts.spacer : '_'; 304 | str = str 305 | .toLowerCase() 306 | .replace(/^\s+/, '') 307 | .replace(/\s+$/, '') 308 | .replace(/\s+/g, '_') 309 | .replace(/[áàâãåäæª]/g, 'a') 310 | .replace(/[éèêëЄ€]/g, 'e') 311 | .replace(/[íìîï]/g, 'i') 312 | .replace(/[óòôõöøº]/g, 'o') 313 | .replace(/[úùûü]/g, 'u') 314 | .replace(/[碩]/g, 'c') 315 | .replace(/[^a-z0-9_\-]/g, '_'); 316 | 317 | if (opts.capitalized) { 318 | split = str.replace(/^_+|_+$/g, '').split(/_+/g); 319 | for (i = 0; i < split.length; i++) { 320 | if (split[i]) split[i] = split[i][0].toUpperCase() + split[i].slice(1); 321 | } 322 | return split.join(spacer); 323 | } 324 | 325 | return str.replace(/^_+|_+$/g, '').replace(/_+/g, spacer); 326 | } 327 | 328 | function text(elm, opts) { 329 | var i, text, children; 330 | opts = opts || {}; 331 | 332 | if (opts.onlyFirst) { 333 | children = elm.childNodes; 334 | text = ''; 335 | 336 | for (i = 0; i < children.length; i++) { 337 | if (children[i].nodeType === 3) { 338 | text += children[i].nodeValue; 339 | } 340 | } 341 | } else { 342 | text = elm.innerText || elm.textContent || elm.innerHTML.replace(/<[^>]+>/g, ''); 343 | } 344 | 345 | return opts.sanitize ? sanitize(text, opts.sanitize) : text; 346 | } 347 | 348 | function getDataLayer(key) { 349 | try { 350 | return google_tag_manager[options.containerId].dataLayer.get(key); 351 | } catch ($$e) { 352 | log('warn', 'Function getDataLayer: Object ' + key + ' is not defined'); 353 | } 354 | } 355 | 356 | function setDataLayer(key, value) { 357 | try { 358 | return google_tag_manager[options.containerId].dataLayer.set(key, value); 359 | } catch ($$e) { 360 | log('warn', $$e); 361 | } 362 | } 363 | 364 | internal.eventQueue = []; 365 | 366 | function event(category, action, label, value, object, id) { 367 | try { 368 | if (internal.sentPageview === false && options.waitQueue) { 369 | log('Info', 'The event (' + arguments + ') has been add to the queue'); 370 | return internal.eventQueue.push(arguments); 371 | } 372 | 373 | if (value != null && typeof value === 'object') { 374 | object = value; 375 | value = undefined; 376 | } else { 377 | object = object || {}; 378 | } 379 | 380 | var result = { 381 | event: options.customNameEvent, 382 | eventCategory: category, 383 | eventAction: action, 384 | eventValue: value, 385 | eventLabel: label, 386 | _tag: id 387 | }; 388 | 389 | if (options.gtmCleanup) { 390 | result.eventCallback = options.gtmCleanup; 391 | } 392 | 393 | log('info', result, object); 394 | window[options.dataLayerName].push(merge(result, object)); 395 | } catch (err) { 396 | log('warn', err); 397 | } 398 | } 399 | 400 | function localHelperFactory(conf) { 401 | var localHelper = { 402 | event: function(category, action, label, value, object) { 403 | return event(category, action, label, value, object, conf.id); 404 | }, 405 | pageview: function(path, object) { 406 | return pageview(path, object, conf.id); 407 | }, 408 | timing: function(category, variable, value, label, object) { 409 | return timing(category, variable, value, label, object, conf.id); 410 | }, 411 | safeFn: function(id, callback, opts) { 412 | return safeFn(conf.id + ':' + id, callback, opts); 413 | }, 414 | on: function(event, selector, callback, parent) { 415 | return on(conf.id, event, selector, callback, parent); 416 | }, 417 | delegate: function(event, selector, callback) { 418 | return on(conf.id, event, selector, callback, document.body); 419 | }, 420 | wrap: function(elm) { 421 | if (typeof elm === 'object' && elm._type === 'wrapped') { 422 | return elm; 423 | } else if (typeof elm === 'string') { 424 | elm = find(window.document, elm); 425 | } else if (elm instanceof HTMLElement) { 426 | elm = [elm]; 427 | } else if ((elm instanceof Array || elm instanceof NodeList) === false) { 428 | throw 'wrap: Esperado receber seletor, elemento HTML, NodeList ou Array'; 429 | } 430 | 431 | return { 432 | _type: 'wrapped', 433 | hasClass: function(className, opts) { 434 | var arr = internalMap(elm, hasClass, [className]); 435 | return opts && opts.toArray ? arr : reduceBool(arr); 436 | }, 437 | matches: function(selector, opts) { 438 | var arr = internalMap(elm, matches, [selector]); 439 | return opts && opts.toArray ? arr : reduceBool(arr); 440 | }, 441 | closest: function(selector) { 442 | return localHelper.wrap(internalMap(elm, closest, [selector])); 443 | }, 444 | text: function(opts) { 445 | var arr = internalMap(elm, text, [opts]); 446 | return opts && opts.toArray ? arr : arr.join(''); 447 | }, 448 | find: function(sel) { 449 | var elms = internalMap(elm, find, [sel]); 450 | return localHelper.wrap(flatten(elms)); 451 | }, 452 | map: function(func, params) { 453 | return internalMap(elm, func, params); 454 | }, 455 | on: function(event, parent, callback) { 456 | if (typeof parent === 'function') { 457 | on(conf.id, event, elm, parent); 458 | } else { 459 | on(conf.id, event, parent, callback, elm); 460 | } 461 | }, 462 | nodes: elm 463 | }; 464 | }, 465 | sanitize: sanitize, 466 | getDataLayer: getDataLayer, 467 | setDataLayer: setDataLayer, 468 | cookie: cookie, 469 | getKey: getKey, 470 | id: conf.id, 471 | args: conf.args, 472 | fn: fn, 473 | log: log, 474 | _event: conf.event, 475 | _selector: conf.selector 476 | }; 477 | return localHelper; 478 | } 479 | 480 | function pageview(path, object, id) { 481 | try { 482 | var result = { 483 | event: options.customNamePageview, 484 | path: path, 485 | _tag: id 486 | }; 487 | 488 | if (options.gtmCleanup) { 489 | result.eventCallback = options.gtmCleanup; 490 | } 491 | 492 | log('info', result, object); 493 | window[options.dataLayerName].push(merge(result, object)); 494 | } catch (err) { 495 | log('warn', err); 496 | } 497 | } 498 | 499 | function safeFn(id, callback, opt) { 500 | opt = opt || {}; 501 | var safe = function() { 502 | try { 503 | callback.call( 504 | this === window ? null : this, 505 | localHelperFactory({ 506 | id: id, 507 | args: arguments, 508 | event: (typeof opt.event === 'string' && opt.event) || undefined, 509 | selector: (typeof opt.selector === 'string' && opt.selector) || undefined 510 | }) 511 | ); 512 | } catch ($$e) { 513 | if (!options.debug) { 514 | if (Math.random() <= options.errorSampleRate) { 515 | window[options.dataLayerName].push({ 516 | event: options.exceptionEvent, 517 | dataQuality: { 518 | category: options.exceptionCategory, 519 | action: id, 520 | label: String($$e), 521 | event: (typeof opt.event === 'string' && opt.event) || undefined, 522 | selector: (typeof opt.selector === 'string' && opt.selector) || undefined 523 | } 524 | }); 525 | } 526 | } else { 527 | log('warn', 'Exception: ', { 528 | exception: $$e, 529 | tag: id, 530 | event: (typeof opt.event === 'string' && opt.event) || undefined, 531 | selector: (typeof opt.selector === 'string' && opt.selector) || undefined 532 | }); 533 | } 534 | } 535 | }; 536 | 537 | return opt.immediate === false ? safe : safe(); 538 | } 539 | internal.timingQueue = []; 540 | 541 | function timing(category, variable, value, label, object, id) { 542 | try { 543 | if (internal.sentPageview === false && options.waitQueue) { 544 | log( 545 | 'Info', 546 | 'The timing event (' + arguments + ') has been add to the queue' 547 | ); 548 | return internal.timingQueue.push(arguments); 549 | } 550 | 551 | object = object || {}; 552 | 553 | var result = { 554 | event: options.customNameTiming, 555 | timingCategory: category, 556 | timingVariable: variable, 557 | timingValue: value, 558 | timingLabel: label, 559 | _tag: id 560 | }; 561 | 562 | if (options.gtmCleanup) { 563 | result.eventCallback = options.gtmCleanup; 564 | } 565 | 566 | log('info', result, object); 567 | window[options.dataLayerName].push(merge(result, object)); 568 | } catch (err) { 569 | log('warn', err); 570 | } 571 | } 572 | function expose() { 573 | if (window[options.helperName] && !options.overwriteHelper) return; 574 | window[options.helperName] = helper; 575 | } 576 | 577 | expose(); 578 | 579 | })(); 580 | -------------------------------------------------------------------------------- /core/modules/closest.js: -------------------------------------------------------------------------------- 1 | function closest(elm, seletor) { 2 | if ('closest' in elm) return elm.closest(seletor); 3 | if (typeof jQuery === 'function') return jQuery(elm).closest(seletor)[0]; 4 | 5 | var parent = elm.parentNode; 6 | 7 | while (parent != document) { 8 | if (matches(parent, seletor)) { 9 | return parent; 10 | } 11 | parent = parent.parentNode; 12 | } 13 | return undefined; 14 | } 15 | 16 | module.exports = closest; 17 | -------------------------------------------------------------------------------- /core/modules/cookie.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Encontra e retorna o cookie com o valor informado por parâmetro 3 | * Esta função é utilizada pela função cookie como facilitadora 4 | * @param {*} key Chave do cookie 5 | */ 6 | function getCookie(key) { 7 | key = '; ' + key + '='; 8 | var cookie = '; ' + document.cookie; 9 | var index = cookie.indexOf(key); 10 | var end; 11 | if (index === -1) { 12 | return ''; 13 | } 14 | cookie = cookie.substring(index + key.length); 15 | end = cookie.indexOf(';'); 16 | return window.unescape(end === -1 ? cookie : cookie.substring(0, end)); 17 | } 18 | 19 | /** 20 | * Função utilizada para atribuir um novo cookie 21 | * Esta função é utilizada pela função cookie como facilitadora 22 | * @param {*} name Nome do cookie 23 | * @param {*} value Valor do cookie 24 | * @param {*} opts Opções do cookie, como vencimento e domínio 25 | */ 26 | function setCookie(name, value, opts) { 27 | var exdate, cookie; 28 | opts = opts || {}; 29 | 30 | cookie = name + '=' + window.escape(value); 31 | if (opts.exdays) { 32 | exdate = new Date(); 33 | exdate.setDate(exdate.getDate() + opts.exdays); 34 | cookie += '; expires=' + exdate.toUTCString(); 35 | } 36 | if (opts.domain) { 37 | cookie += '; domain=' + opts.domain; 38 | } 39 | cookie += '; path=' + (opts.path || '/'); 40 | return (document.cookie = cookie); 41 | } 42 | 43 | /** 44 | * Função exposta que pode recuperar ou criar um novo cookie 45 | * Caso somente o primeiro parâmetro 'name' seja informado, 46 | * a função irá procurar um cookie com este parâmetro. 47 | * Caso o usuário também informe o parâmetro 'value', a função 48 | * irá criar um novo cookie. 49 | * @param {*} name Nome do Cookie 50 | * @param {*} value Valor do Cookie 51 | * @param {*} opts Opções do cookie, como vencimento e domínio 52 | */ 53 | function cookie(name, value, opts) { 54 | if (typeof value === 'undefined') 55 | return getCookie(name); 56 | 57 | return setCookie(name, value, opts); 58 | } 59 | 60 | module.exports = { 61 | getCookie: getCookie, 62 | setCookie: setCookie, 63 | cookie: cookie 64 | }; 65 | -------------------------------------------------------------------------------- /core/modules/delegate.js: -------------------------------------------------------------------------------- 1 | function delegate(id, event, selector, oldHandler, parent) { 2 | var method, elm, handler; 3 | if (typeof jQuery === 'function') { 4 | elm = jQuery(parent || document); 5 | handler = safeFn(id, oldHandler, { 6 | event: event, 7 | selector: selector, 8 | immediate: false 9 | }); 10 | if (typeof elm.on === 'function') { 11 | return elm.on(event, selector, handler); 12 | } else if (typeof elm.delegate === 'function') { 13 | return elm.delegate(selector, event, handler); 14 | } 15 | } 16 | 17 | if (typeof parent === 'string') { 18 | parent = document.querySelectorAll(parent); 19 | } 20 | 21 | if (typeof document.addEventListener === 'function') { 22 | method = 'addEventListener'; 23 | } else { 24 | method = 'attachEvent'; 25 | event = 'on' + event; 26 | } 27 | 28 | handler = function(e) { 29 | for ( 30 | var target = e.target; 31 | target && target !== this; 32 | target = target.parentNode 33 | ) { 34 | if (matches(target, selector)) { 35 | var handler = safeFn(id, oldHandler, { 36 | event: event, 37 | selector: selector, 38 | immediate: false 39 | }); 40 | handler.call(target, e); 41 | break; 42 | } 43 | } 44 | }; 45 | 46 | if (Object.prototype.toString.call(parent) === '[object NodeList]') { 47 | for (var parentIndex = 0; parentIndex <= parent.length - 1; parentIndex++) { 48 | (parent[parentIndex] || document)[method](event, handler, false); 49 | } 50 | } else { 51 | (parent || document)[method](event, handler, false); 52 | } 53 | } 54 | 55 | module.exports = delegate; 56 | -------------------------------------------------------------------------------- /core/modules/find.js: -------------------------------------------------------------------------------- 1 | function find(element, selector) { 2 | return element.querySelectorAll(selector); 3 | } 4 | 5 | module.exports = find; 6 | -------------------------------------------------------------------------------- /core/modules/flatten.js: -------------------------------------------------------------------------------- 1 | function flatten(arrs) { 2 | var currentArray, currentElement, i, j; 3 | var result = []; 4 | 5 | if (arrs.length === 1) return arrs[0]; 6 | 7 | while (arrs.length > 0) { 8 | currentArray = arrs.shift(); 9 | for (i = 0; currentArray.length > i; i++) { 10 | currentElement = currentArray[i]; 11 | j = 0; 12 | while (j < result.length && currentElement !== result[j]) { 13 | j += 1; 14 | } 15 | if (j === result.length) result.push(currentElement); 16 | } 17 | } 18 | 19 | return result; 20 | } 21 | 22 | module.exports = flatten; 23 | -------------------------------------------------------------------------------- /core/modules/getkey.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Recupera o valor de uma chave encadeada de um objeto, 3 | * tratando o erro de acessar um objeto de undefined em qualquer 4 | * passo da cadeia de objetos 5 | * @param {*} key Chave para recuperar o seu valor 6 | * @param {*} opt_root Caso houver a necessidade de passar o objeto 7 | * root deste elemento. 8 | */ 9 | function getKey(key, opt_root) { 10 | if (!key || typeof key !== 'string') return undefined; 11 | 12 | var result = opt_root || window; 13 | var splitKey = key.split('.'); 14 | 15 | for (var i = 0; i < splitKey.length && result != null; i++) { 16 | if (has(result, splitKey[i])) { 17 | result = result[splitKey[i]]; 18 | } else { 19 | return undefined; 20 | } 21 | } 22 | return result; 23 | } 24 | 25 | module.exports = getKey; 26 | -------------------------------------------------------------------------------- /core/modules/has.js: -------------------------------------------------------------------------------- 1 | function has(obj, key) { 2 | return hasOwnProperty.call(obj, key); 3 | } 4 | 5 | module.exports = has; 6 | -------------------------------------------------------------------------------- /core/modules/hasClass.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Procura no atributo class de um dado elemento 3 | * alguma classe que contenha determinado nome. 4 | * Caso exista, retorna true caso haja e 5 | * false caso contrário. 6 | * @param {*} e Elemento no qual se realiza a verificação 7 | * @param {*} className Classe para qual verifica-se a 8 | * existência no elemento 9 | */ 10 | function hasClass(e, className) { 11 | if ('classList' in e) return e.classList.contains(className); 12 | 13 | return new RegExp('\\b' + className + '\\b').test(e.className); 14 | } 15 | 16 | module.exports = hasClass; 17 | -------------------------------------------------------------------------------- /core/modules/init.js: -------------------------------------------------------------------------------- 1 | function init(opt_options) { 2 | options = merge(options, opt_options); 3 | expose(); 4 | } 5 | 6 | module.exports = init; 7 | -------------------------------------------------------------------------------- /core/modules/internalMap.js: -------------------------------------------------------------------------------- 1 | function internalMap(elms, func, exArgs) { 2 | var elm, args; 3 | var ret = []; 4 | for (var index = 0; index < elms.length; index++) { 5 | elm = elms[index]; 6 | if (elm instanceof HTMLElement === false) 7 | throw 'internalMap: Esperado elemento HTML'; 8 | args = [elm].concat(exArgs); 9 | ret.push(func.apply(null, args)); 10 | } 11 | return ret; 12 | } 13 | 14 | module.exports = internalMap; 15 | -------------------------------------------------------------------------------- /core/modules/log.js: -------------------------------------------------------------------------------- 1 | function log(type, info, obj) { 2 | if (options.debug && typeof getKey('console.' + type) === 'function') { 3 | console[type](info, obj); 4 | } 5 | } 6 | 7 | module.exports = log; 8 | -------------------------------------------------------------------------------- /core/modules/matches.js: -------------------------------------------------------------------------------- 1 | function matches(elm, seletor) { 2 | if ('matches' in elm) return elm.matches(seletor); 3 | if (typeof jQuery === 'function') return jQuery(elm).is(seletor); 4 | 5 | var elms = elm.parentNode.querySelectorAll(seletor); 6 | 7 | for (var i = 0; i < elms.length; i++) { 8 | if (elms[i] === elm) { 9 | return true; 10 | } 11 | } 12 | return false; 13 | } 14 | 15 | module.exports = matches; 16 | -------------------------------------------------------------------------------- /core/modules/merge.js: -------------------------------------------------------------------------------- 1 | function merge(obj, obj2) { 2 | if (obj2) { 3 | for (var key in obj2) { 4 | if (has(obj2, key)) { 5 | obj[key] = obj2[key]; 6 | } 7 | } 8 | } 9 | return obj; 10 | } 11 | 12 | module.exports = merge; 13 | -------------------------------------------------------------------------------- /core/modules/on.js: -------------------------------------------------------------------------------- 1 | function on(id, event, selector, oldCallback, parent) { 2 | var i, array, elm, callback; 3 | 4 | if (parent) return delegate(id, event, selector, oldCallback, parent); 5 | 6 | callback = safeFn(id, oldCallback, { 7 | event: event, 8 | selector: selector, 9 | immediate: false 10 | }); 11 | 12 | if (typeof jQuery === 'function') { 13 | elm = jQuery(selector); 14 | 15 | if (typeof elm.on === 'function') { 16 | return elm.on(event, callback); 17 | } else if (typeof elm.bind === 'function') { 18 | return elm.bind(event, callback); 19 | } 20 | } 21 | 22 | if (typeof selector === 'string') { 23 | array = document.querySelectorAll(selector); 24 | } else if (typeof selector.length === 'undefined' || selector === window) { 25 | array = [selector]; 26 | } else { 27 | array = selector; 28 | } 29 | 30 | for (i = 0; i < array.length; i++) { 31 | elm = array[i]; 32 | 33 | if (typeof elm.addEventListener === 'function') { 34 | elm.addEventListener(event, callback); 35 | } else { 36 | elm.attachEvent('on' + event, callback); 37 | } 38 | } 39 | } 40 | 41 | module.exports = on; 42 | -------------------------------------------------------------------------------- /core/modules/reduceBool.js: -------------------------------------------------------------------------------- 1 | function reduceBool(arr) { 2 | var i; 3 | for (i = 0; i < arr.length; i++) { 4 | if (arr[i]) return true; 5 | } 6 | return false; 7 | } 8 | 9 | module.exports = reduceBool; 10 | -------------------------------------------------------------------------------- /core/modules/sanitize.js: -------------------------------------------------------------------------------- 1 | function sanitize(str, opts) { 2 | var split, i, spacer; 3 | 4 | if (!str) return ''; 5 | opts = opts || {}; 6 | spacer = typeof opts.spacer === 'string' ? opts.spacer : '_'; 7 | str = str 8 | .toLowerCase() 9 | .replace(/^\s+/, '') 10 | .replace(/\s+$/, '') 11 | .replace(/\s+/g, '_') 12 | .replace(/[áàâãåäæª]/g, 'a') 13 | .replace(/[éèêëЄ€]/g, 'e') 14 | .replace(/[íìîï]/g, 'i') 15 | .replace(/[óòôõöøº]/g, 'o') 16 | .replace(/[úùûü]/g, 'u') 17 | .replace(/[碩]/g, 'c') 18 | .replace(/[^a-z0-9_\-]/g, '_'); 19 | 20 | if (opts.capitalized) { 21 | split = str.replace(/^_+|_+$/g, '').split(/_+/g); 22 | for (i = 0; i < split.length; i++) { 23 | if (split[i]) split[i] = split[i][0].toUpperCase() + split[i].slice(1); 24 | } 25 | return split.join(spacer); 26 | } 27 | 28 | return str.replace(/^_+|_+$/g, '').replace(/_+/g, spacer); 29 | } 30 | 31 | module.exports = sanitize; 32 | -------------------------------------------------------------------------------- /core/modules/text.js: -------------------------------------------------------------------------------- 1 | function text(elm, opts) { 2 | var i, text, children; 3 | opts = opts || {}; 4 | 5 | if (opts.onlyFirst) { 6 | children = elm.childNodes; 7 | text = ''; 8 | 9 | for (i = 0; i < children.length; i++) { 10 | if (children[i].nodeType === 3) { 11 | text += children[i].nodeValue; 12 | } 13 | } 14 | } else { 15 | text = elm.innerText || elm.textContent || elm.innerHTML.replace(/<[^>]+>/g, ''); 16 | } 17 | 18 | return opts.sanitize ? sanitize(text, opts.sanitize) : text; 19 | } 20 | 21 | module.exports = text; 22 | -------------------------------------------------------------------------------- /documentation-images/event_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DP6/analytics-helper/4f2e2c4352736cdb2a2d918b46d1110907c04b7f/documentation-images/event_name.png -------------------------------------------------------------------------------- /documentation-images/once_per_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DP6/analytics-helper/4f2e2c4352736cdb2a2d918b46d1110907c04b7f/documentation-images/once_per_page.png -------------------------------------------------------------------------------- /documentation-images/tag_dataquality.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DP6/analytics-helper/4f2e2c4352736cdb2a2d918b46d1110907c04b7f/documentation-images/tag_dataquality.png -------------------------------------------------------------------------------- /documentation-images/tag_event.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DP6/analytics-helper/4f2e2c4352736cdb2a2d918b46d1110907c04b7f/documentation-images/tag_event.png -------------------------------------------------------------------------------- /documentation-images/tag_pageview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DP6/analytics-helper/4f2e2c4352736cdb2a2d918b46d1110907c04b7f/documentation-images/tag_pageview.png -------------------------------------------------------------------------------- /documentation-images/tag_timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DP6/analytics-helper/4f2e2c4352736cdb2a2d918b46d1110907c04b7f/documentation-images/tag_timing.png -------------------------------------------------------------------------------- /documentation-images/var_gasettings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DP6/analytics-helper/4f2e2c4352736cdb2a2d918b46d1110907c04b7f/documentation-images/var_gasettings.png -------------------------------------------------------------------------------- /gtm/expose.js: -------------------------------------------------------------------------------- 1 | function expose() { 2 | if (window[options.helperName] && !options.overwriteHelper) return; 3 | window[options.helperName] = helper; 4 | } 5 | 6 | expose(); 7 | -------------------------------------------------------------------------------- /gtm/globalVars.js: -------------------------------------------------------------------------------- 1 | var hasOwnProperty = Object.prototype.hasOwnProperty; 2 | var jQuery = window.jQuery; 3 | var fn = {}; 4 | 5 | var options = { 6 | helperName: 'analyticsHelper', 7 | dataLayerName: 'dataLayer', 8 | debug: ({{Debug Mode}} || false), 9 | waitQueue: true, 10 | containerId: ({{Container ID}} || ''), 11 | exceptionEvent: 'gtm_dataQuality_event', 12 | exceptionCategory: 'GTM Exception', 13 | customNamePageview: 'ga_pageview', 14 | customNameEvent: 'ga_event', 15 | customNameTiming: 'ga_timing', 16 | errorSampleRate: 1, 17 | gtmCleanup: function (gtmId) { 18 | helper.setDataLayer('ecommerce', undefined); 19 | helper.setDataLayer('noInteraction', undefined); 20 | } 21 | }; 22 | 23 | var internal = { 24 | sentPageview: false 25 | }; 26 | 27 | var helper = { 28 | internal: internal, 29 | init: init, 30 | pageview: pageview, 31 | event: event, 32 | timing: timing, 33 | sanitize: sanitize, 34 | getDataLayer: getDataLayer, 35 | setDataLayer: setDataLayer, 36 | cookie: cookie, 37 | getKey: getKey, 38 | safeFn: safeFn, 39 | fn: fn, 40 | options: options 41 | }; 42 | -------------------------------------------------------------------------------- /gtm/main.js: -------------------------------------------------------------------------------- 1 | ; 2 | (function () { 3 | 'use strict'; 4 | //=require globalVars.js 5 | //=require gtm-modules.js 6 | //=require expose.js 7 | })(); 8 | -------------------------------------------------------------------------------- /gtm/modules/dataLayer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Recupera uma chave do dataLayer utilizando o objeto 3 | * padrão do GTM 'google_tag_manager' 4 | * Obs: Possui dependência com a ativação da variável 'container ID' 5 | * @param {*} key 6 | */ 7 | function getDataLayer(key) { 8 | try { 9 | return google_tag_manager[options.containerId].dataLayer.get(key); 10 | } catch ($$e) { 11 | log('warn', 'Function getDataLayer: Object ' + key + ' is not defined'); 12 | } 13 | } 14 | 15 | /** 16 | * Define uma chave do dataLayer utilizando o objeto 17 | * padrão do GTM 'google_tag_manager' 18 | * Obs: Possui dependência com a ativação da variável 'container ID' 19 | * @param {*} key 20 | */ 21 | function setDataLayer(key, value) { 22 | try { 23 | return google_tag_manager[options.containerId].dataLayer.set(key, value); 24 | } catch ($$e) { 25 | log('warn', $$e); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /gtm/modules/event.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Disparo personalizado de eventos 3 | * @param {*} category Categoria do evento 4 | * @param {*} action Ação do evento 5 | * @param {*} label Rótulo do evento 6 | * @param {*} value Valor do evento 7 | * @param {*} object Objeto para ser inserido no dataLayer 8 | * que pode ser utilizado para Enhanced Ecommerce, dentre outros. 9 | */ 10 | internal.eventQueue = []; 11 | 12 | function event(category, action, label, value, object, id) { 13 | try { 14 | if (internal.sentPageview === false && options.waitQueue) { 15 | log('Info', 'The event (' + arguments + ') has been add to the queue'); 16 | return internal.eventQueue.push(arguments); 17 | } 18 | 19 | if (value != null && typeof value === 'object') { 20 | object = value; 21 | value = undefined; 22 | } else { 23 | object = object || {}; 24 | } 25 | 26 | var result = { 27 | event: options.customNameEvent, 28 | eventCategory: category, 29 | eventAction: action, 30 | eventValue: value, 31 | eventLabel: label, 32 | _tag: id 33 | }; 34 | 35 | if (options.gtmCleanup) { 36 | result.eventCallback = options.gtmCleanup; 37 | } 38 | 39 | log('info', result, object); 40 | window[options.dataLayerName].push(merge(result, object)); 41 | } catch (err) { 42 | log('warn', err); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /gtm/modules/localHelperFactory.js: -------------------------------------------------------------------------------- 1 | function localHelperFactory(conf) { 2 | var localHelper = { 3 | event: function(category, action, label, value, object) { 4 | return event(category, action, label, value, object, conf.id); 5 | }, 6 | pageview: function(path, object) { 7 | return pageview(path, object, conf.id); 8 | }, 9 | timing: function(category, variable, value, label, object) { 10 | return timing(category, variable, value, label, object, conf.id); 11 | }, 12 | safeFn: function(id, callback, opts) { 13 | return safeFn(conf.id + ':' + id, callback, opts); 14 | }, 15 | on: function(event, selector, callback, parent) { 16 | return on(conf.id, event, selector, callback, parent); 17 | }, 18 | delegate: function(event, selector, callback) { 19 | return on(conf.id, event, selector, callback, document.body); 20 | }, 21 | wrap: function(elm) { 22 | if (typeof elm === 'object' && elm._type === 'wrapped') { 23 | return elm; 24 | } else if (typeof elm === 'string') { 25 | elm = find(window.document, elm); 26 | } else if (elm instanceof HTMLElement) { 27 | elm = [elm]; 28 | } else if ((elm instanceof Array || elm instanceof NodeList) === false) { 29 | throw 'wrap: Esperado receber seletor, elemento HTML, NodeList ou Array'; 30 | } 31 | 32 | return { 33 | _type: 'wrapped', 34 | hasClass: function(className, opts) { 35 | var arr = internalMap(elm, hasClass, [className]); 36 | return opts && opts.toArray ? arr : reduceBool(arr); 37 | }, 38 | matches: function(selector, opts) { 39 | var arr = internalMap(elm, matches, [selector]); 40 | return opts && opts.toArray ? arr : reduceBool(arr); 41 | }, 42 | closest: function(selector) { 43 | return localHelper.wrap(internalMap(elm, closest, [selector])); 44 | }, 45 | text: function(opts) { 46 | var arr = internalMap(elm, text, [opts]); 47 | return opts && opts.toArray ? arr : arr.join(''); 48 | }, 49 | find: function(sel) { 50 | var elms = internalMap(elm, find, [sel]); 51 | return localHelper.wrap(flatten(elms)); 52 | }, 53 | map: function(func, params) { 54 | return internalMap(elm, func, params); 55 | }, 56 | on: function(event, parent, callback) { 57 | if (typeof parent === 'function') { 58 | on(conf.id, event, elm, parent); 59 | } else { 60 | on(conf.id, event, parent, callback, elm); 61 | } 62 | }, 63 | nodes: elm 64 | }; 65 | }, 66 | sanitize: sanitize, 67 | getDataLayer: getDataLayer, 68 | setDataLayer: setDataLayer, 69 | cookie: cookie, 70 | getKey: getKey, 71 | id: conf.id, 72 | args: conf.args, 73 | fn: fn, 74 | log: log, 75 | _event: conf.event, 76 | _selector: conf.selector 77 | }; 78 | return localHelper; 79 | } 80 | -------------------------------------------------------------------------------- /gtm/modules/pageview.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utilizado para disparar pageviews virtuais 3 | * @param {*} path Valor do pagepath do disparo 4 | * @param {*} object Objeto para ser inserido no dataLayer 5 | * que pode ser utilizado para Enhanced Ecommerce, dentre outros. 6 | */ 7 | function pageview(path, object, id) { 8 | try { 9 | var result = { 10 | event: options.customNamePageview, 11 | path: path, 12 | _tag: id 13 | }; 14 | 15 | if (options.gtmCleanup) { 16 | result.eventCallback = options.gtmCleanup; 17 | } 18 | 19 | log('info', result, object); 20 | window[options.dataLayerName].push(merge(result, object)); 21 | } catch (err) { 22 | log('warn', err); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /gtm/modules/safeFn.js: -------------------------------------------------------------------------------- 1 | function safeFn(id, callback, opt) { 2 | opt = opt || {}; 3 | var safe = function() { 4 | try { 5 | callback.call( 6 | this === window ? null : this, 7 | localHelperFactory({ 8 | id: id, 9 | args: arguments, 10 | event: (typeof opt.event === 'string' && opt.event) || undefined, 11 | selector: 12 | (typeof opt.selector === 'string' && opt.selector) || undefined 13 | }) 14 | ); 15 | } catch ($$e) { 16 | if (!options.debug) { 17 | if (Math.random() <= options.errorSampleRate) { 18 | window[options.dataLayerName].push({ 19 | event: options.exceptionEvent, 20 | dataQuality: { 21 | category: options.exceptionCategory, 22 | action: id, 23 | label: String($$e), 24 | event: (typeof opt.event === 'string' && opt.event) || undefined, 25 | selector: 26 | (typeof opt.selector === 'string' && opt.selector) || undefined 27 | } 28 | }); 29 | } 30 | } else { 31 | log('warn', 'Exception: ', { 32 | exception: $$e, 33 | tag: id, 34 | event: (typeof opt.event === 'string' && opt.event) || undefined, 35 | selector: 36 | (typeof opt.selector === 'string' && opt.selector) || undefined 37 | }); 38 | } 39 | } 40 | }; 41 | 42 | return opt.immediate === false ? safe : safe(); 43 | } -------------------------------------------------------------------------------- /gtm/modules/timing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Disparo personalizado de timing 3 | * @param {*} category Categoria do evento 4 | * @param {*} action Ação do evento 5 | * @param {*} label Rótulo do evento 6 | * @param {*} value Valor do evento 7 | * @param {*} object Objeto para ser inserido no dataLayer 8 | * que pode ser utilizado para Enhanced Ecommerce, dentre outros. 9 | */ 10 | internal.timingQueue = []; 11 | 12 | function timing(category, variable, value, label, object, id) { 13 | try { 14 | if (internal.sentPageview === false && options.waitQueue) { 15 | log( 16 | 'Info', 17 | 'The timing event (' + arguments + ') has been add to the queue' 18 | ); 19 | return internal.timingQueue.push(arguments); 20 | } 21 | 22 | object = object || {}; 23 | 24 | var result = { 25 | event: options.customNameTiming, 26 | timingCategory: category, 27 | timingVariable: variable, 28 | timingValue: value, 29 | timingLabel: label, 30 | _tag: id 31 | }; 32 | 33 | if (options.gtmCleanup) { 34 | result.eventCallback = options.gtmCleanup; 35 | } 36 | 37 | log('info', result, object); 38 | window[options.dataLayerName].push(merge(result, object)); 39 | } catch (err) { 40 | log('warn', err); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const gulp = require('gulp'); 4 | const concat = require('gulp-concat'); 5 | const beautify = require('gulp-beautify'); 6 | const include = require('gulp-include'); 7 | const strip = require('gulp-strip-comments'); 8 | const del = require('del'); 9 | const replace = require('gulp-replace'); 10 | 11 | gulp.task('gtm-modules', () => 12 | gulp.src(['./core/modules/*.js', './gtm/modules/*js']) 13 | .pipe(replace(/module.exports[a-z-A-Z.]*\s*=\s*([a-zA-Z\-_]+;|([a-zA-Z\-_:.={},\n\s]+);+)/g, '')) 14 | .pipe(concat('gtm-modules.js')) 15 | .pipe(beautify({ 16 | indent_size: 2, 17 | max_preserve_newlines: 2 18 | })) 19 | .pipe(strip()) 20 | .on('error', console.error) 21 | .pipe(gulp.dest('./tmp')) 22 | ); 23 | 24 | gulp.task('build-gtm', () => 25 | gulp.src('./gtm/main.js') 26 | .pipe(replace(/module.exports\s*=\s*[a-zA-Z]+;/g, '')) 27 | .pipe(include({ 28 | hardFail: true, 29 | includePaths: [ 30 | __dirname + '/tmp', 31 | __dirname + '/gtm', 32 | ] 33 | })) 34 | .on('error', console.error) 35 | .pipe(gulp.dest('./build/gtm')) 36 | ); 37 | 38 | gulp.task('clean', () => del(['./tmp'])); 39 | 40 | gulp.task('default', gulp.series(['clean', 'gtm-modules', 'build-gtm', 'clean'])); 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "analytics-helper", 3 | "version": "1.0.1", 4 | "description": "> Biblioteca auxiliar para implementação dos Tag Managers da DP6.", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/DP6/analytics-helper.git" 9 | }, 10 | "author": "", 11 | "contributors": [ 12 | { 13 | "name": "Bruno Munhoz", 14 | "email": "bpm1993@gmail.com" 15 | }, 16 | { 17 | "name": "Paulo Brumatti", 18 | "email": "paulo8624@gmail.com" 19 | } 20 | ], 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/DP6/analytics-helper/issues" 24 | }, 25 | "homepage": "https://github.com/DP6/analytics-helper#readme", 26 | "scripts": { 27 | "test": "cross-env NODE_ENV=test nyc --reporter=lcov --reporter=text-summary --reporter=text mocha test/index.test.js --require @babel/register", 28 | "coverage": "nyc report --reporter=text-lcov | coveralls" 29 | }, 30 | "nyc": { 31 | "check-coverage": true, 32 | "per-file": true, 33 | "lines": [ 34 | 80, 35 | 99 36 | ], 37 | "statements": [ 38 | 90, 39 | 99 40 | ], 41 | "functions": [ 42 | 90, 43 | 99 44 | ], 45 | "branches": [ 46 | 80, 47 | 99 48 | ], 49 | "exclude": [ 50 | "build", 51 | "documentation-images" 52 | ], 53 | "include": [ 54 | "core/**/*.js", 55 | "gtm/*.js", 56 | "gtm/**/*.js" 57 | ], 58 | "reporter": [ 59 | "lcov", 60 | "text-summary", 61 | "text" 62 | ], 63 | "require": [ 64 | "@babel/register" 65 | ], 66 | "sourceMap": false, 67 | "instrument": false 68 | }, 69 | "release": { 70 | "branch": "master", 71 | "verifyConditions": [ 72 | "@semantic-release/github" 73 | ], 74 | "publish": [ 75 | "@semantic-release/github" 76 | ], 77 | "prepare": [], 78 | "success": [ 79 | "@semantic-release/github" 80 | ], 81 | "fail": [ 82 | "@semantic-release/github" 83 | ] 84 | }, 85 | "devDependencies": { 86 | "@babel/cli": "^7.12.8", 87 | "@babel/core": "^7.12.9", 88 | "@babel/preset-env": "^7.12.7", 89 | "@babel/register": "^7.12.1", 90 | "babel-plugin-istanbul": "^6.0.0", 91 | "chai": "^4.2.0", 92 | "commitizen": "^4.2.2", 93 | "coveralls": "^3.1.0", 94 | "cross-env": "^7.0.3", 95 | "cz-conventional-changelog": "^3.3.0", 96 | "del": "^6.0.0", 97 | "gulp": "^4.0.2", 98 | "gulp-beautify": "^3.0.0", 99 | "gulp-concat": "^2.6.1", 100 | "gulp-include": "^2.4.1", 101 | "gulp-minify": "^3.1.0", 102 | "gulp-replace": "^1.0.0", 103 | "gulp-strip-comments": "^2.5.2", 104 | "gulp-uglify": "^3.0.2", 105 | "jsdom": "^16.4.0", 106 | "mocha": "^8.2.1", 107 | "nyc": "^15.1.0", 108 | "sonarqube-scanner": "^2.8.0" 109 | }, 110 | "dependencies": {}, 111 | "config": { 112 | "commitizen": { 113 | "path": "./node_modules/cz-conventional-changelog" 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /test/core/modules/closet.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const closet = require('../../.././core/modules/closet.js'); 5 | 6 | describe('Core', () => { 7 | describe('Modules', () => { 8 | describe('.closest(elm, seletor)', () => { 9 | it('should export a function', () => { 10 | expect(closet).to.be.a('function'); 11 | }); 12 | }); 13 | }); 14 | }); -------------------------------------------------------------------------------- /test/core/modules/cookie.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const cookie = require('../../.././core/modules/cookie.js'); 5 | 6 | describe('Core', () => { 7 | describe('Modules', () => { 8 | describe('.cookie(name, value, opts)', () => { 9 | it('should export a function', () => { 10 | expect(cookie.cookie).to.be.a('function'); 11 | }); 12 | }); 13 | 14 | describe('.getCookie(key)', () => { 15 | it('should export a function', () => { 16 | expect(cookie.setCookie).to.be.a('function'); 17 | }); 18 | }); 19 | }); 20 | }); -------------------------------------------------------------------------------- /test/core/modules/has.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const has = require('../../.././core/modules/has.js'); 5 | 6 | describe('Core', () => { 7 | describe('Modules', () => { 8 | describe('.has(obj, key)', () => { 9 | it('should export a function', () => { 10 | expect(has).to.be.a('function'); 11 | }); 12 | 13 | it('should return true if object has attribute with name of the key', () => { 14 | expect(has({ name: 'foo' }, 'name')).true; 15 | }); 16 | }); 17 | }); 18 | }); -------------------------------------------------------------------------------- /test/core/modules/sanitize.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const sanitize = require('../../.././core/modules/sanitize.js'); 5 | 6 | describe('Core', () => { 7 | describe('Modules', () => { 8 | it('should export a function', () => { 9 | expect(sanitize).to.be.a('function'); 10 | }); 11 | 12 | describe('.sanitize(text)', () => { 13 | it('should replace special characters for normalize String to DP6 pattern', () => { 14 | expect(sanitize('O cão é do DONO')).to.equal('o_cao_e_do_dono'); 15 | }); 16 | 17 | it('should return a empty String if text is not truthey', () => { 18 | expect(sanitize()).to.be.empty; 19 | }); 20 | 21 | }); 22 | 23 | describe('.sanitize(text, opts)', () => { 24 | it('should return String with spacer equal to |', () => { 25 | expect(sanitize('bringing science to digital marketing DP6', { spacer: '|' })).to.equal('bringing|science|to|digital|marketing|dp6'); 26 | }); 27 | 28 | it('should return capitalized String', () => { 29 | expect(sanitize('bringing science to digital marketing DP6', { capitalized: true, spacer: ' ' })).to.equal('Bringing Science To Digital Marketing Dp6'); 30 | }); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/core/modules/text.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const sanitize = require('../../.././core/modules/sanitize.js'); 5 | const text = require('../../.././core/modules/text.js'); 6 | const domMock = require('../.././jsdom-site-mock.js'); 7 | 8 | describe('Core', () => { 9 | describe('Modules', () => { 10 | describe('.text(element)', () => { 11 | it('should export a function', () => { 12 | expect(text).to.be.a('function'); 13 | }); 14 | }); 15 | 16 | }); 17 | }); -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./core/modules/sanitize.test.js'); 4 | require('./core/modules/has.test.js'); 5 | require('./core/modules/text.test.js'); 6 | require('./core/modules/cookie.test.js'); -------------------------------------------------------------------------------- /test/jsdom-site-mock.js: -------------------------------------------------------------------------------- 1 | const jsdom = require('jsdom'); 2 | const fs = require('fs'); 3 | const virtualConsole = new jsdom.VirtualConsole(); 4 | const { JSDOM } = jsdom; 5 | 6 | const dom = {}; //new JSDOM(fs.readFileSync('./.././test/src/site/index.html').toString(), { virtualConsole }); 7 | 8 | module.export = dom; -------------------------------------------------------------------------------- /test/src/site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Product example for Bootstrap 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 36 | 37 |
38 |
39 |

Punny headline

40 |

And an even wittier subheading to boot. Jumpstart your marketing efforts with this example based on Apple's marketing pages.

41 | Coming soon 42 |
43 |
44 |
45 |
46 | 47 |
48 |
49 |
50 |

Another headline

51 |

And an even wittier subheading.

52 |
53 |
54 |
55 |
56 |
57 |

Another headline

58 |

And an even wittier subheading.

59 |
60 |
61 |
62 |
63 | 64 |
65 |
66 |
67 |

Another headline

68 |

And an even wittier subheading.

69 |
70 |
71 |
72 |
73 |
74 |

Another headline

75 |

And an even wittier subheading.

76 |
77 |
78 |
79 |
80 | 81 |
82 |
83 |
84 |

Another headline

85 |

And an even wittier subheading.

86 |
87 |
88 |
89 |
90 |
91 |

Another headline

92 |

And an even wittier subheading.

93 |
94 |
95 |
96 |
97 | 98 |
99 |
100 |
101 |

Another headline

102 |

And an even wittier subheading.

103 |
104 |
105 |
106 |
107 |
108 |

Another headline

109 |

And an even wittier subheading.

110 |
111 |
112 |
113 |
114 | 115 | 161 | 162 | 163 | 165 | 166 | 167 | 170 | 171 | 172 | 173 | 180 | 181 | 182 | --------------------------------------------------------------------------------