@azzas/azzas-tracker-web
v2.0.7
Published
Camada única de **data tracking** das lojas web. A loja chama `trackWebEvent`, a lib resolve os parâmetros a partir dos dados brutos da plataforma (produto, orderForm, SLAs...) e faz o push no `dataLayer` no formato esperado pelo GTM/GA4.
Readme
azzas-tracker-web
Camada única de data tracking das lojas web. A loja chama trackWebEvent, a lib resolve os parâmetros a partir dos dados brutos da plataforma (produto, orderForm, SLAs...) e faz o push no dataLayer no formato esperado pelo GTM/GA4.
Toda a inteligência (formatação, normalização, atribuição de vitrine, promoção) fica aqui, não nos repositórios das lojas.
Instalação
NPM
npm install @azzas/azzas-tracker-webimport { trackWebEvent, initTracker } from '@azzas/azzas-tracker-web'CDN / script tag (expõe o global AzzasTracker)
<script src="https://cdn.jsdelivr.net/npm/@azzas/azzas-tracker-web/dist/mod.global.js"></script>
<script>
AzzasTracker.trackWebEvent('SEARCH', { adapter: 'META', meta: { search_term: 'vestido', search_found: true, search_quantity: 42 } })
</script>| Build | Uso |
|---|---|
| dist/mod.js / dist/mod.cjs | ESM / CJS (import via npm) |
| dist/mod.global.js | IIFE ES2018, global AzzasTracker (CDN) |
| dist/mod.vtex.global.js | IIFE ES5, sem quebrar o Uglify do checkout VTEX legado |
Uso
1. Configure uma vez
initTracker({ brand: 'reserva', currency: 'BRL' })brand e currency daqui têm prioridade sobre qualquer valor vindo do payload. Se não chamar, brand é resolvido pelo adapter e currency cai em BRL.
2. Dispare eventos com trackWebEvent
trackWebEvent(EVENT, payload)EVENT: chave deEVENTS('VIEW_ITEM_LIST','ADD_TO_CART','PURCHASE'...).payload:{ adapter, ...dados brutos, meta, window? }. Oadapterdiz de onde vêm os dados, a lib extrai os parâmetros.metacarrega o que não dá pra derivar dos dados de um produto (região, termo de busca, banner clicado...).
Por que existe um adapter por loja
O dataLayer recebe sempre o mesmo formato de items, mas cada loja entrega a entidade de produto com um shape diferente. O adapter é a tradução entre os dois. Mesmo campo, caminho diferente em cada origem:
| Campo | DECO | RESERVA / ANIMALE | HEADLESS (PDP) | HEADLESS (PDC) | NV | ORDERFORM |
|---|---|---|---|---|---|---|
| item_name | alternateName | isVariantOf.name | productName | name | productName | name sem o skuName |
| item_id | inProductGroupWithID | isVariantOf.productGroupID | productId | isVariantOf.productGroupID | productId | productId |
| item_ref | additionalProperty[RefId] | gtin | productReference | não existe no retorno, vai null | productReference | productRefId |
| seller_id | offers.offers[0].seller | offers.offers[].seller.identifier (Animale: o disponível, senão o primeiro) | items[0].sellers[0].sellerId | offers.offers[].seller.identifier (primeiro disponível) | seller default do item ativo | seller |
| tamanho | name.split(' - ')[1] | Reserva: name.split(' - ')[1] / Animale: additionalProperty[Tamanho] | items[0].name.split(' - ')[1] | additionalProperty[Tamanho] | name.split(' - ')[1] do item ativo | skuName.split(' - ')[1] |
Repare no HEADLESS: a mesma loja entrega produto num shape na PDP (retorno da search API, com items[]) e em outro na PDC (JSON-LD com isVariantOf). Por isso alguns adapters têm zone. Sem isso, um único getter teria que adivinhar em qual formato o produto veio.
Como um adapter nasce: o types.ts de cada adapter é o type da entidade de produto copiado direto da loja, não uma modelagem nossa. Antes de criar, a pergunta pro time da loja é: "a entidade de produto é uma só, padronizada no site inteiro, ou muda dependendo de onde está sendo usada?". Se muda, cada variação vira uma zone. Se algum campo não existe na origem (ex.: item_ref na PDC do headless), o getter devolve null em vez de inventar.
A tipagem (PayloadFor<E>) garante em tempo de compilação que:
- eventos com
itemsnão aceitamadapter: 'META'; - os campos de
metaobrigatórios pro evento são exigidos.
Exemplos
productsprecisa ter exatamente o shape que o adapter espera. O type de produto de cada adapter está emsrc/params/adapters/<adapter>/types.tse foi copiado direto da loja. A tipagem detrackWebEventé inferida peloadapterinformado: se o objeto que você passa não bate com esse type, o TS acusa erro na própria chamada. Não adapte o produto na mão pra "passar": se o shape da loja mudou, o adapter é que precisa ser atualizado. Os únicos campos extras aceitos sãoindexeitem_list_name.
Vitrine (Deco)
trackWebEvent('VIEW_ITEM_LIST', {
adapter: 'DECO',
products: products.map((p, index) => ({ ...p, index, item_list_name: 'Vitrine Home' })),
meta: { region: 'home' },
})Clique no produto
trackWebEvent('SELECT_ITEM', {
adapter: 'RESERVA',
products: [{ ...product, index, item_list_name: 'Lançamentos' }],
meta: { region: 'home' },
})Checkout (orderForm VTEX)
trackWebEvent('ADD_SHIPPING_INFO', {
adapter: 'ORDERFORM',
orderForm: vtexjs.checkout.orderForm,
meta: { pre_filled: false },
})Evento sem produto
trackWebEvent('SEARCH', {
adapter: 'META',
meta: { search_term: 'vestido', search_found: true, search_quantity: 42 },
})Banner promocional (ver Promotion)
trackWebEvent('SELECT_PROMOTION', {
adapter: 'META',
meta: {
region: 'home',
promotion_name: 'Black Friday',
creative_name: 'banner-hero',
creative_slot: 'slot-1',
promotion_url: '/colecao/black-friday', // só pro match interno, não vai pro dataLayer
},
})Dados do usuário
trackWebEvent('CUSTOM_USER_INFO', {
adapter: 'META',
meta: { user_info: { account: 'lojausereserva', platform: 'store', userEmail: '[email protected]' } },
})Formulário (manuallyInfo)
// checkout/shipping: manda só a fatia de endereço
trackWebEvent('CUSTOM_USER_INFO', {
adapter: 'META',
meta: { user_info: { account: 'lojafarm', platform: 'store', userEmail: '[email protected]',
manuallyInfo: { address: { street: 'Av Paulista', number: '1000', postalCode: '01310-100' } },
}},
})user_info consulta /_v/user-info no domínio da conta (account × platform, ver PROD_DOMAINS) e devolve customer + address. CPF sempre sai com hash SHA-256 em cpf e limpo em document.
O manuallyInfo é o que o usuário acabou de digitar, então entra por cima do Master Data, campo a campo:
- Cada formulário manda só a sua fatia.
checkout/profileeaccount/profilemandam os campos pessoais no nível de cima;checkout/shippingmandaaddress. A fatia que o formulário não cobre continua vindo do Master Data. - Só sobrescreve o que foi preenchido.
'',nulleundefinedsão ignorados e o valor do Master Data permanece.falseconta como preenchido, senão o opt-out nunca chegaria no CRM. - Se a API falhar, sobra o
manuallyInfomais email/id do contexto.
A rota /_v/user-info não é da VTEX: é um app VTEX IO nosso, instalado em cada conta. Ele recebe o email e consulta o Master Data com AppKey/AppToken da conta:
| Entidade | Busca | Campos |
|---|---|---|
| CL (cliente) | email | birthDate, phone, homePhone, firstName, lastName, gender, id, document, userId |
| AD (endereço) | userId do cliente | city, country, neighborhood, street, postalCode, number, state |
Por isso a lista de domínios em PROD_DOMAINS: a chamada precisa ir pro host onde o app está instalado (.myvtex.com na loja, secure.* no checkout) pra não cair em CORS. Conta nova = instalar o app e adicionar o domínio aqui.
É necessário configurar a apiToken e apiKey nas configurações da app.
Como funciona
trackWebEvent(EVENT, payload)
→ EVENTS[EVENT].requiredParams quais parâmetros o evento precisa
→ adapters[payload.adapter][param]() cada getter extrai um parâmetro dos dados brutos
→ itemsWithPromotion(items) enriquece items com promoção (se houver)
→ pushToDataLayer(event, parameters) push no window.dataLayerflowchart LR
Store["Loja<br/>trackWebEvent(EVENT, payload)"] --> Events["EVENTS[EVENT]<br/>requiredParams"]
Events --> Formatter["formatter<br/>getParameters()"]
Formatter --> Adapter["adapters[payload.adapter]<br/>1 getter por param"]
Adapter --> Params["parameters"]
Params -->|"tem items?"| Promo["itemsWithPromotion()<br/>enriquece com banner"]
Promo --> Push["pushToDataLayer()"]
Params --> Push
Push --> DL[("window.dataLayer")]
Store -.->|"SELECT_PROMOTION"| Cookie[("cookie<br/>__azzas_trk_promo")]
Cookie -.-> Promo
Store -.->|"PURCHASE"| Clear["limpa cookie"]Push no dataLayer:
| hasEcommerce | Formato |
|---|---|
| true | { ecommerce: null } seguido de { event, ecommerce: { ...params } } |
| false | { event, ...params } |
Erros nunca quebram a loja: são capturados e logados como [DT] Error tracking event .... Getter faltando pro adapter loga [DT] Missing getter "x" for adapter "Y" e o parâmetro vai como null.
Adapters
Todos herdam o META (getters que leem só de meta) e sobrescrevem o que depende dos dados brutos.
| Adapter | Payload esperado | Origem |
|---|---|---|
| META | { meta } | Eventos sem produto |
| DECO | { products } (JSON-LD Product do Deco) | Lojas Deco |
| RESERVA | { products } | Reserva |
| ANIMALE | { zone: 'PDP' \| 'PDC', products } | Animale |
| HEADLESS | { zone: 'PDP' \| 'PDC', products } | Lojas headless VTEX (zona Minicart tipada, ainda não roteada no adapter) |
| NV | { products } (VTEX search API) | ByNV |
| ORDERFORM | { orderForm } | Checkout VTEX (carrinho, frete, pagamento, compra) |
| PICKUP | { flag_pickup, shippings } (SLAs) | Consulta de CEP |
Todo payload aceita brand como override pontual e window (pra push em outro contexto, ex.: iframe).
Alguns getters leem campos da raiz do payload, não de meta (não estão na tipagem, passe como any se precisar):
| Campo | Getter | Usado em |
|---|---|---|
| appliedCoupon | coupon | ADD_COUPON, PURCHASE |
| appliedSellerCodName | seller_cod_name | ADD_COUPON, PURCHASE |
| couponMessage | coupon_message | ADD_COUPON (fallback: orderForm.messages com "cupom") |
| items, value, line_items, total_discount, available_grid | idem | override pronto, pula a extração |
Em products, cada item pode levar index e item_list_name — é isso que alimenta items[].index, items[].item_list_name e o match de promoção.
meta
| Campo | Usado em |
|---|---|
| region | quase todos (home, pdp, minicart...) |
| pre_filled | checkout (add_personal_info, add_shipping_info, add_payment_info) |
| search_term, search_found, search_quantity | SEARCH |
| zipcode | SEARCH_ZIPCODE |
| promotion_name, creative_name, creative_slot, promotion_url | VIEW_PROMOTION, SELECT_PROMOTION |
| content_type | SELECT_CONTENT |
| method, type | AUTH_ACTION |
| item_ref, size | NOTIFY_ME |
| color, size, price_range, category, ordering | REFINE_RESULTS |
| slot_per_line | GRID_SIZE |
| user_info | CUSTOM_USER_INFO |
| brand | override quando initTracker não foi chamado |
Eventos
Chave → nome no dataLayer → parâmetros. Fonte: src/core/constants.ts.
Catálogo / navegação
| Evento | event | Params |
|---|---|---|
| VIEW_ITEM_LIST | view_item_list | region, brand, line_items, items |
| SELECT_ITEM | select_item | brand, region, items, line_items |
| VIEW_ITEM / CUSTOM_VIEW_ITEM | view_item / custom_view_item | brand, line_items, currency, value, available_grid, items |
| SEARCH | search | brand, search_term, search_found, search_quantity |
| REFINE_RESULTS | refine_results | brand, region, color, price_range, size, category, ordering |
| GRID_SIZE | grid_size | brand, slot_per_line |
| SELECT_CONTENT | select_content | brand, content_type |
| NOTIFY_ME | notify_me | brand, size, item_ref |
| VIEW_PROMOTION | view_promotion | brand, region, currency, promotion_name, creative_slot, creative_name |
| SELECT_PROMOTION | select_promotion | brand, region, currency, promotion_name, creative_slot, creative_name |
Carrinho
| Evento | event | Params |
|---|---|---|
| ADD_TO_CART / CUSTOM_ADD_TO_CART | add_to_cart / custom_add_to_cart | brand, region, line_items, currency, value, items |
| REMOVE_FROM_CART / CUSTOM_REMOVE_FROM_CART | remove_from_cart / custom_remove_from_cart | brand, region, line_items, currency, value, items |
| ADD_TO_WISHLIST | add_to_wishlist | brand, region, line_items, currency, value, items |
| CUSTOM_VIEW_CART | custom_view_cart | brand, region, line_items, currency, value, total_discount, subtotal, items |
| ADD_COUPON | add_coupon | brand, region, coupon, coupon_message, seller_cod_name |
| SEARCH_ZIPCODE | search_zipcode | brand, region, shippings, zipcode, flag_pickup |
Checkout
| Evento | event | Params |
|---|---|---|
| BEGIN_CHECKOUT / CUSTOM_BEGIN_CHECKOUT | begin_checkout / custom_begin_checkout | brand, line_items, currency, value, total_discount, subtotal, items |
| ADD_PERSONAL_INFO | add_personal_info | brand, pre_filled, line_items, currency, value, total_discount, subtotal |
| ADD_SHIPPING_INFO / CUSTOM_ADD_SHIPPING_INFO | add_shipping_info / custom_add_shipping_info | brand, pre_filled, shipping, shipping_tier, line_items, currency, value, total_discount, subtotal, items |
| ADD_PAYMENT_INFO / CUSTOM_ADD_PAYMENT_INFO | add_payment_info / custom_add_payment_info | brand, pre_filled, line_items, currency, value, total_discount, payment_type, subtotal, items |
| ORDER_REVIEWED | order_reviewed | brand, line_items, currency, value, total_discount, subtotal, payment_type |
| PURCHASE / CUSTOM_PURCHASE | purchase / custom_purchase | brand, shipping_tier, line_items, currency, value, shipping, transaction_id, total_discount, payment_type, seller_cod_name, subtotal, coupon, items |
Outros
| Evento | event | Params |
|---|---|---|
| AUTH_ACTION | auth_action | region, method, type, brand |
| CUSTOM_USER_INFO | custom_user_info | user_info |
Variantes CUSTOM_* têm os mesmos params do evento padrão e só mudam o nome no dataLayer. Use quando a VTEX já dispara nativamente o evento com o mesmo nome (acontece na loja e no checkout), pra não duplicar no GTM.
Formato de items
| Campo | Descrição |
|---|---|
| item_id / item_sku / item_ref | productId / skuId / referência |
| item_name, item_brand | nome e marca (capitalizada em Reserva/NV/orderForm, minúscula no Deco; Farm normalizada pra farmrio / farmetc) |
| item_category, item_category2 | outlet | bazar | sale | Coleção; categoria da árvore |
| item_variant, item_variant2 | cor e tamanho (tamanho em maiúsculo) |
| price, discount, quantity, index | preço final, desconto (lista − final), qtd, posição na lista |
| item_url, image_url, seller_id | link, imagem (500x500), seller |
| item_list_name, item_shipping_tier | vitrine de origem, frete do item (checkout) |
| promotion_name, creative_name, creative_slot | preenchidos pelo Promotion quando o item veio de um banner |
Promotion
Em teste (rollout) em Reserva, Farm e Animale. A lógica é agnóstica de adapter e roda pra qualquer loja que dispare SELECT_PROMOTION; o rollout é sobre quais lojas já emitem o evento.
Objetivo: atribuir ao produto o banner que originou o clique, mesmo que a compra aconteça em outra página ou subdomínio (loja → checkout).
Fluxo
SELECT_PROMOTIONsalva a promoção no cookie__azzas_trk_promo(domínio raiz, vale em todos os subdomínios).VIEW_PROMOTIONsó reporta, não grava.- Em qualquer evento com
items, cada item tenta casar com uma promoção salva:- por vitrine:
item_list_namedo item ==pathnamedapromotion_urldo banner; - por anotação: o item já foi vinculado a uma promoção antes.
- por vitrine:
- Casou → o item recebe
promotion_name,creative_name,creative_slot. PURCHASE/CUSTOM_PURCHASElimpa o cookie.
Anotação: só SELECT_ITEM, ADD_TO_CART e CUSTOM_ADD_TO_CART gravam no cookie de qual promoção o item veio (intenção real). VIEW_ITEM_LIST não anota de propósito. É a anotação que mantém a atribuição no carrinho/checkout, onde não existe item_list_name de vitrine.
| Regra | Valor | |---|---| | Janela pra casar por vitrine | 10 min após o clique no banner | | Promoção com item anotado | vive até 24h (ou até o purchase) | | Máx. de promoções no cookie | 5 (descarta a mais antiga) | | Máx. de itens anotados | 30 (descarta os mais antigos) | | Vitrine casa com 2 promoções | vale a do clique mais recente |
promotion_url nunca vai pro dataLayer; existe só pro match. Código: src/core/promotion.ts.
Debug
Adicione ?__trk_inspect__ na URL. Cada trackWebEvent loga no console [DT vX.Y.Z] EVENT com { context, parameters }.
Desenvolvimento
npm run build # tsup → dist/
npm run dev:npm # build em watch
npm run dev:deno # build + pack + serve em http://localhost:4507 (simula CDN)Pra testar via CDN no consumidor, aponte o script pra http://localhost:4507/dist/mod.global.js.
Publicar
npm run release:homolog # prerelease -preview.N, tag "preview"
npm run release:prod # patch + publishContribuindo
- Novo evento: adicionar em
EVENTScomname,hasEcommerce,requiredParams. Todo param listado precisa existir como getter nos adapters que vão disparar o evento, senão sainull(com erro no console). - Novo parâmetro: criar o getter no
METAse vier só demeta; nos adapters específicos se depender dos dados brutos. - Novo adapter: pasta em
src/params/adapters/<nome>/comindex.ts(...metaAdapter+ getters),types.ts; registrar emadaptersnoformatter.tse no unionAdapterPayloademtypes.ts. - Nos consumidores: mande os dados o mais crus possível. Tratamento é responsabilidade da lib.
- Documente eventos novos no Notion.
Authors
- Lucas Soares
