@funnel10x/tracking 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,8 +7,9 @@ no npm como pacote público, pela versão.
7
7
 
8
8
  O que faz numa página:
9
9
 
10
- - **o PostHog** (o posthog-js, embutido), com os ids de anúncio como parâmetros
11
- de campanha, e **cada evento limpo antes de sair**: o URL fica com o host, o
10
+ - **o PostHog** (o posthog-js, embutido: o snippet **é** o PostHog, e numa
11
+ página que já tenha outro não arranca, ver o checklist), com os ids de
12
+ anúncio como parâmetros de campanha, e **cada evento limpo antes de sair**: o URL fica com o host, o
12
13
  caminho e só os parâmetros de campanha da allow-list (`src/clean.js`), no
13
14
  evento, no perfil (`$set`, `$set_once`), no cookie do PostHog, nos heatmaps
14
15
  e na gravação de sessão (onde os pedidos de rede se gravam sem o corpo). O
@@ -22,13 +23,15 @@ O que faz numa página:
22
23
  e nos links do checkout;
23
24
  - **o identify pelo id do contacto** num link de email ou de SMS
24
25
  (`?contact_id={{contact.id}}`);
25
- - **o email e o telefone da pessoa, quando ela os dá** (a 1.1.0, como a
26
- HYROS): do URL, quando o funil os passa à página seguinte (`email`, `phone`,
27
- `phoneNumber`, `phone_number`, `tel`, `telefone`), e dos campos de email e de
28
- telefone de qualquer form da página, à saída do campo e no envio. O telefone
26
+ - **o email e o telefone que a pessoa envia** (desde a 1.1.0, como a HYROS;
27
+ só no envio desde a 1.2.0): do URL, quando o funil os passa à página seguinte
28
+ (`email`, `phone`, `phoneNumber`, `phone_number`, `tel`, `telefone`), e dos
29
+ campos de email e de telefone do form que ela envia (o `submit` de um
30
+ `<form>`, ou o botão de enviar do form nativo do GHL). O de um form que ela
31
+ não enviou não sai do browser. O telefone
29
32
  ganha o indicativo do seletor de país (o intl-tel-input), que é o que um
30
33
  seletor costuma esconder. Vão num evento `contact_captured` (com
31
- `contact_source`: `url`, `field` ou `form`), cada valor uma vez por página, e
34
+ `contact_source`: `url` ou `form`), cada valor uma vez por página, e
32
35
  só com o PostHog a medir. Não vão ao perfil (`$set`), que passaria o browser
33
36
  a pessoa identificada, o escalão mais caro do PostHog: o perfil recebe-os do
34
37
  identify do servidor, uma vez por contacto;
@@ -106,26 +109,54 @@ Num funil do GHL ou num site em Webflow, onde não há build, o mesmo pacote
106
109
  carrega-se por uma tag Custom HTML do GTM (em All Pages), **pela versão fixa**
107
110
  no URL do jsDelivr e com o hash do ficheiro no `integrity`. A tag de cada versão
108
111
  publicada está no próprio pacote, em `dist/gtm.html`, e cola-se tal como está:
109
- só se trocam a chave e os hosts do checkout.
112
+ só se trocam a chave, o `apiHost` (se a account tiver o proxy gerido, abaixo) e
113
+ os hosts do checkout.
110
114
 
111
115
  ```html
112
116
  <script>
113
117
  window.f10xTracking = {
114
118
  key: '{{PostHog project key}}',
119
+ apiHost: '',
115
120
  checkoutHosts: []
116
121
  }
117
122
  </script>
118
- <script src="https://cdn.jsdelivr.net/npm/@funnel10x/tracking@1.0.0/dist/tag.js" integrity="sha384-…" crossorigin="anonymous"></script>
123
+ <script src="https://cdn.jsdelivr.net/npm/@funnel10x/tracking@1.3.0/dist/tag.js" integrity="sha384-…" crossorigin="anonymous"></script>
119
124
  ```
120
125
 
121
- Sem `site.json` não há interruptor: o snippet corre como a tag o deixar (as
122
- condições de consentimento da tag no GTM, se o site tiver um banner). Uma versão
123
- nova é uma tag nova, nunca `@1` nem `@latest`: o GTM de cada site diz que versão
124
- corre, e o `tracking_version` de cada evento confirma-o.
126
+ Uma versão nova é uma tag nova, nunca `@1` nem `@latest`: o GTM de cada site
127
+ diz que versão corre, e o `tracking_version` de cada evento confirma-o.
128
+
129
+ **O proxy gerido do PostHog.** Sem build não há o `/_t`, e com o `apiHost`
130
+ vazio os envios vão para o `us.i.posthog.com`, que a EasyPrivacy bloqueia: a
131
+ visita de quem tem um bloqueador não chega ao PostHog. O PostHog tem um proxy
132
+ gerido, um subdomínio da account (um CNAME para o PostHog, nas definições da
133
+ organização, *Managed reverse proxy*) que recebe os envios e os passa ao
134
+ PostHog. Com ele, o `apiHost` da tag é esse subdomínio: na IR,
135
+ `https://p.infinityresponse.com`, e na IMO Circle, `https://p.imocircle.com`
136
+ (os dois válidos, lidos no PostHog a 2026-10-08; o Piko ainda não tem). As listas que bloqueiam pelo nome do domínio deixam de o apanhar;
137
+ o uBlock Origin no Firefox, que lê o CNAME, apanha na mesma, e quanto se
138
+ recupera não se mede, porque o que é bloqueado não chega para se contar. O
139
+ subdomínio é de outra origem que a página, e por isso o browser só lhe manda a
140
+ origem no `Referer`, e não o URL com a query string.
141
+
142
+ **O consentimento.** Sem `site.json` o snippet não tem interruptor, e quem
143
+ decide se ele corre é o sítio onde a tag está. No GTM, corre como as condições
144
+ de consentimento da tag deixarem (o banner ligado ao GTM); no *Head tracking
145
+ code* do GHL, ou no campo de código de outra plataforma, corre sempre, também
146
+ para quem recusou. Por isso, **num site com banner, a tag fica no GTM, com a
147
+ condição do consentimento de analytics; num site sem banner, pode ir para o
148
+ head**, o que a livra de um GTM bloqueado (a EasyPrivacy bloqueia o
149
+ `googletagmanager.com`). É uma regra de onde se põe a tag, e não código.
125
150
 
126
151
  **Numa página de um funil do GHL, o form é nativo** (sem `<form>`, cada campo
127
- com o query key em `data-q`): o snippet escreve nos campos cujo query key é um
128
- dos valores, e o GHL envia-os. **Um form do GHL embebido noutro site corre num
152
+ com o query key em `data-q`), e **um campo escondido só se enche pelo URL com
153
+ que a página carrega**: o GHL lê o query key quando a app arranca, e envia o
154
+ estado dele, a que o campo não está ligado (medido a 2026-10-07: um valor
155
+ escrito no campo depois, mesmo com o evento `input`, não chega ao envio, e a tag
156
+ do *Head tracking code* corre já depois do arranque). Por isso, num funil do
157
+ GHL, a ponte entre o dispositivo e o contacto é o email que a pessoa envia (o
158
+ `contact_captured`), que o admin liga ao contacto criado nesse momento; o campo
159
+ `ph_anon_id` só se enche quando o id já vem no URL. **Um form do GHL embebido noutro site corre num
129
160
  iframe** de outro domínio, onde o script da página
130
161
  não chega. O snippet passa ao iframe, pelo URL dele, os ids e os parâmetros do
131
162
  anúncio, e os campos escondidos do form lêem-nos de lá (cada campo com o
@@ -137,24 +168,28 @@ ao form o que veio no URL dele.
137
168
 
138
169
  O que os forms, o iframe e os links do checkout levam:
139
170
 
140
- | campo | o que é | sem consentimento |
141
- | --- | --- | --- |
142
- | `ph_anon_id` | o id do dispositivo no PostHog (o anónimo de origem) | não |
143
- | `ph_distinct_id` | o distinct id agora (o id do contacto, depois do identify) | não |
144
- | `ph_session_id` | a sessão do PostHog | não |
145
- | `contact_id` | o id do contacto que veio num link desta sessão | não |
146
- | as UTMs | `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, `utm_id`, `utm_placement` | sim, do URL |
147
- | os click ids | `fbclid`, `gclid`, `gbraid`, `wbraid`, `gad_source`, `gad_campaignid`, `msclkid`, `ctwa_clid` | sim, do URL |
148
- | os ids de anúncio | `ad_id`, `adset_id`, `campaign_id`, `adgroup_id`, e os da HYROS `h_ad_id`, `fbc_id` | sim, do URL |
149
- | `lp` | a página de entrada que o URL declara | sim, do URL |
171
+ | campo | o que é | sem consentimento | campo custom no GHL |
172
+ | --- | --- | --- | --- |
173
+ | `ph_anon_id` | o id do dispositivo no PostHog (o anónimo de origem) | não | opcional: só se enche quando o id chega no URL |
174
+ | `ph_distinct_id` | o distinct id agora (o id do contacto, depois do identify) | não | não |
175
+ | `ph_session_id` | a sessão do PostHog | não | não |
176
+ | `contact_id` | o id do contacto que veio num link desta sessão | não | não |
177
+ | as UTMs | `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, `utm_id`, `utm_placement` | sim, do URL | só `utm_source`, `utm_medium` e `utm_campaign`, nas locations de um site `direct` com consentimento |
178
+ | os click ids | `fbclid`, `gclid`, `gbraid`, `wbraid`, `gad_source`, `gad_campaignid`, `msclkid`, `ctwa_clid` | sim, do URL | não |
179
+ | os ids de anúncio | `a_campaign_id`, `a_adset_id`, `a_ad_id` (a convenção, abaixo), e os da HYROS `h_ad_id`, `fbc_id` | sim, do URL | só `a_ad_id`, como as UTMs |
180
+ | `lp` | a página de entrada que o URL declara | sim, do URL | não |
150
181
 
151
182
  - **Num form da página**, um campo que já existe com o nome recebe o valor se
152
183
  for escondido ou estiver vazio; os que faltam acrescentam-se escondidos, só
153
184
  num form de lead (com um campo de email ou de telefone) e nunca num com
154
185
  `data-f10x-skip`. Voltam a preencher-se no envio, com os valores desse
155
186
  momento.
156
- - **Num form do GHL**, os campos custom do contacto com estes nomes, que a A5
157
- lê.
187
+ - **Num form do GHL** não há campos a pôr: a ponte para o contacto é o email
188
+ que a pessoa envia (o `contact_captured`), e o click id do contacto é a
189
+ atribuição nativa do GHL, que a A5 lê. Os quatro campos de atribuição da
190
+ tabela só existem nas locations que recebam leads de um site `direct` com
191
+ `consent: true`: quem recusa não tem PostHog, e um contacto criado pelo
192
+ webhook não tem atribuição nativa (plano, A2 revista a 2026-10-07).
158
193
  - **Nos links do checkout** (os do Stripe e os hosts em `checkoutHosts`), os ids
159
194
  sobrepõem-se ao que o link traga, e os parâmetros do anúncio só entram se o
160
195
  link não tiver os seus. Num Payment Link do Stripe vai também o
@@ -167,6 +202,8 @@ O que os forms, o iframe e os links do checkout levam:
167
202
 
168
203
  `init(options)` (ou a tag) devolve, e põe em `window.f10xTracking`:
169
204
 
205
+ - `mode`: `tag` ou `direct`; `frame` num iframe, `preview` numa
206
+ pré-visualização, e `conflict` quando a página já tem outro PostHog;
170
207
  - `fields()`: os valores de cima, agora;
171
208
  - `grant()`, `deny()`, `consent()`: o interruptor de um site `direct`;
172
209
  - `onReady(fn)`: quando o PostHog estiver a medir;
@@ -176,14 +213,37 @@ O que os forms, o iframe e os links do checkout levam:
176
213
 
177
214
  ## Os modelos de parâmetros de URL
178
215
 
216
+ **Os nomes vão nas UTMs, os ids em três parâmetros nossos**, com o prefixo
217
+ `a_` (de *attribution*), como a HYROS tem o `h_`, e desde a 1.3.0. Um nome é
218
+ editável no gestor de anúncios e um rename não dá erro nenhum; o id nasce com
219
+ o anúncio. Os três querem dizer o mesmo em todas as plataformas, e a plataforma
220
+ diz-se no `utm_source`:
221
+
222
+ | nível | parâmetro | Meta | Google |
223
+ | --- | --- | --- | --- |
224
+ | a campanha | `a_campaign_id` | `{{campaign.id}}` | `{campaignid}` |
225
+ | o conjunto (o ad set da Meta, o ad group da Google) | `a_adset_id` | `{{adset.id}}` | `{adgroupid}` |
226
+ | o anúncio | `a_ad_id` | `{{ad.id}}` | `{creative}` |
227
+
228
+ Na HYROS, o `h_ad_id` é o anúncio em todas, mas o nível de cima muda de nome e
229
+ de nível com a plataforma: o `fbc_id` é o ad set na Meta, o `gc_id` é a
230
+ campanha na Google; na Meta a campanha não vai, e na Google o ad group também
231
+ não. O `h_ad_id` e o `fbc_id` continuam aceites, pelos anúncios da IMO Circle
232
+ que ainda os trazem (4.324 pageviews desde abril, medidos a 2026-10-08). Os
233
+ nomes sem prefixo da 1.0.0 à 1.2.0 (`ad_id`, `adset_id`, `campaign_id`,
234
+ `adgroup_id`) saíram: nenhum anúncio os usou. Nenhum URL das três accounts
235
+ trazia já um parâmetro `a_` (medido no mesmo dia).
236
+
179
237
  **Meta**, nos *URL parameters* de cada anúncio:
180
238
 
181
239
  ```
182
- utm_source=facebook&utm_medium=paid_social&utm_campaign={{campaign.name}}&utm_term={{adset.name}}&utm_content={{ad.name}}&utm_id={{campaign.id}}&utm_placement={{placement}}&campaign_id={{campaign.id}}&adset_id={{adset.id}}&ad_id={{ad.id}}
240
+ utm_source=meta&utm_medium=paid_social&utm_campaign={{campaign.name}}&utm_term={{adset.name}}&utm_content={{ad.name}}&utm_id={{campaign.id}}&utm_placement={{placement}}&a_campaign_id={{campaign.id}}&a_adset_id={{adset.id}}&a_ad_id={{ad.id}}
183
241
  ```
184
242
 
185
243
  O nome do anúncio acaba no código do asset (decisão 10 do plano), e é por isso
186
- que vai no `utm_content`.
244
+ que vai no `utm_content`. O `utm_source` é `meta`, como na Traffic Sources e nos
245
+ anúncios das três accounts (45.819 pageviews com `fbclid` em 90 dias, medidos a
246
+ 2026-10-08); até à 1.2.0 este modelo dizia `facebook`, que nenhum usava.
187
247
 
188
248
  **Google**, no *Final URL suffix* da conta, com o auto-tagging ligado (o
189
249
  `gclid`) e dois parâmetros personalizados: `campaign` em cada campanha (o nome
@@ -191,9 +251,19 @@ dela) e `asset` em cada anúncio (o código do asset, que num anúncio de pesqui
191
251
  num responsive display ou na Performance Max não chega pelo nome):
192
252
 
193
253
  ```
194
- utm_source=google&utm_medium=cpc&utm_campaign={_campaign}&utm_term={keyword}&utm_content={_asset}&campaign_id={campaignid}&adgroup_id={adgroupid}&ad_id={creative}
254
+ utm_source=google&utm_medium=cpc&utm_campaign={_campaign}&utm_term={keyword}&utm_content={_asset}&a_campaign_id={campaignid}&a_adset_id={adgroupid}&a_ad_id={creative}
195
255
  ```
196
256
 
257
+ ## Os links orgânicos
258
+
259
+ Um link que não é anúncio (a bio do Instagram, a descrição de um vídeo, um
260
+ email, o WhatsApp, um parceiro) leva as UTMs da fonte dele, pelas regras da
261
+ tabela **Traffic Sources** do admin, que é nomenclatura partilhada por todas as
262
+ accounts. Sem elas a visita chega sem origem, e o referrer de um browser in-app
263
+ ou de um cliente de email vem quase sempre vazio, o que a conta como *direct*.
264
+ Este README não copia a tabela, que é a fonte; o gerador de links da Marketing
265
+ Ops, que os monta a partir dela, está no plano da Marketing (Fase M48).
266
+
197
267
  ## O checklist de um funil
198
268
 
199
269
  Antes de ligar o tráfego:
@@ -201,18 +271,27 @@ Antes de ligar o tráfego:
201
271
  1. **O snippet carrega pela versão**: num repo `direct`, a versão exacta no
202
272
  `package.json`, a chave na variável de ambiente e os rewrites do `/_t` no
203
273
  `vercel.json`; num site de fora, a tag do `dist/gtm.html` da versão, com a
204
- chave da account, no GTM.
205
- 2. **O consentimento**: o `consent` do `site.json` escrito, e o banner a chamar
206
- `grant()` e `deny()`; num site de fora, as condições da tag no GTM.
207
- 3. **Os forms**: cada form do GHL com os campos escondidos da tabela, com o
208
- *query key* igual ao nome; o workflow do identify no envio; e um envio de
209
- teste com `?utm_source=teste&gclid=teste` deixa os campos preenchidos no
210
- contacto.
211
- 4. **Os links de email e de SMS** levam `contact_id={{contact.id}}`.
212
- 5. **O checkout**: o host em `checkoutHosts` (o Stripe já lá está), e num
274
+ chave da account e, se ela tiver o proxy gerido, o `apiHost`.
275
+ 2. **Nenhum outro PostHog na página**: a tag ou o snippet do PostHog que o site
276
+ já tenha (no GTM, no head, no código do site) sai quando este entra. Este
277
+ snippet já é o PostHog, e com outro na página não arranca (avisa na
278
+ consola, e o `window.f10xTracking.mode` fica `conflict`). Com a mesma chave
279
+ não se perde nada: quem volta mantém o id anónimo, e o URL inicial que o
280
+ outro deixou no cookie reescreve-se limpo.
281
+ 3. **O consentimento**: o `consent` do `site.json` escrito, e o banner a chamar
282
+ `grant()` e `deny()`; num site de fora, a tag no GTM com a condição do
283
+ consentimento se o site tiver banner, e no head só se não tiver.
284
+ 4. **Os forms**: um envio de teste, escrito à mão numa página aberta com
285
+ `?utm_source=teste`, deixa no PostHog um `contact_captured` com o email,
286
+ no momento do envio, e o contacto no GHL; um email escrito e não enviado não
287
+ deixa nada. Num site `direct` com consentimento, a location tem os quatro
288
+ campos de atribuição da tabela, com o *query key* igual ao nome.
289
+ 5. **Os links de email e de SMS** levam `contact_id={{contact.id}}`.
290
+ 6. **O checkout**: o host em `checkoutHosts` (o Stripe já lá está), e num
213
291
  checkout em código os `fields()` na metadata.
214
- 6. **Os anúncios**: os parâmetros de cima na Meta e na Google.
215
- 7. **A prova**: um pageview aberto com `?email=teste@example.com&utm_source=teste`
292
+ 7. **Os anúncios**: os parâmetros de cima na Meta e na Google, e os links
293
+ orgânicos com as UTMs da Traffic Sources.
294
+ 8. **A prova**: um pageview aberto com `?email=teste@example.com&utm_source=teste`
216
295
  chega ao PostHog (Activity, no projeto da account) com a `utm_source` e sem
217
296
  o `email`, no evento e no perfil, e com o `tracking_version` da versão.
218
297
 
package/dist/gtm.html CHANGED
@@ -1,8 +1,9 @@
1
- <!-- @funnel10x/tracking@1.1.0: a tag do GTM (Custom HTML, em All Pages). Só se muda a chave; uma versão nova é um ficheiro novo. -->
1
+ <!-- @funnel10x/tracking@1.3.0: a tag do GTM (Custom HTML, em All Pages). Só se mudam a chave e, se a account tiver o proxy gerido do PostHog, o apiHost; uma versão nova é um ficheiro novo. -->
2
2
  <script>
3
3
  window.f10xTracking = {
4
4
  key: '{{PostHog project key}}',
5
+ apiHost: '',
5
6
  checkoutHosts: []
6
7
  }
7
8
  </script>
8
- <script src="https://cdn.jsdelivr.net/npm/@funnel10x/tracking@1.1.0/dist/tag.js" integrity="sha384-2ACrLX/l5RYZ+55HKpvkNM5uzAhAqQPoGWoGqj3w4mKUcSAMKYV8glWCJBbSDDD+" crossorigin="anonymous"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/@funnel10x/tracking@1.3.0/dist/tag.js" integrity="sha384-BFUxhoktzA6AWF2yGaTdVq+krqyK0WqpmJfPAP3ga7hqHXxJu+WXr2lLbx6CH6G0" crossorigin="anonymous"></script>
package/dist/index.d.ts CHANGED
@@ -21,7 +21,7 @@ export interface TrackingOptions {
21
21
 
22
22
  export interface Tracking {
23
23
  version: string
24
- mode: 'direct' | 'tag' | 'preview' | 'frame'
24
+ mode: 'direct' | 'tag' | 'preview' | 'frame' | 'conflict'
25
25
  consent(): 'granted' | 'denied' | null
26
26
  grant(): void
27
27
  deny(): void