@funnel10x/tracking 1.2.0 → 1.4.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
@@ -39,6 +40,15 @@ O que faz numa página:
39
40
  **interruptor de consentimento** do `site.json`, e a guarda das
40
41
  pré-visualizações.
41
42
 
43
+ **Quem lê isto do lado do admin** (Fase A3 do plano da atribuição): a fonte
44
+ PostHog da ingestão guarda o `tracking_version`, a `page_version`, o `ab_test`
45
+ e o `fingerprint` de cada evento, e o email e o telefone do `contact_captured`
46
+ só em hash. O histórico de um funil, que chegou ao PostHog antes deste snippet
47
+ com o URL inteiro, é aparado lá pela mesma allow-list do `src/clean.js`: um
48
+ parâmetro que entre na `ALLOWED_PARAMS` entra também na entrada `posthog` da
49
+ `ingest/pii.json` e na `supabase/verify/atr3.sql` do admin, no mesmo PR (um
50
+ teste do admin prova que as três listas são a mesma).
51
+
42
52
  O posthog-js e o FingerprintJS vão dentro do ficheiro, pela versão que este
43
53
  pacote fixa, e não se resolvem no site. O que fica de fora são as extensões que
44
54
  o posthog-js carrega quando o projeto as liga (o gravador de sessão, os dead
@@ -62,7 +72,7 @@ init({
62
72
  apiHost: '/_t',
63
73
  consent: site.consent,
64
74
  pageVersion: 3,
65
- test: '/pt/f1/lp#2',
75
+ test: '/pt/acq_f1/webinar/lp#2',
66
76
  })
67
77
  ```
68
78
 
@@ -108,22 +118,45 @@ Num funil do GHL ou num site em Webflow, onde não há build, o mesmo pacote
108
118
  carrega-se por uma tag Custom HTML do GTM (em All Pages), **pela versão fixa**
109
119
  no URL do jsDelivr e com o hash do ficheiro no `integrity`. A tag de cada versão
110
120
  publicada está no próprio pacote, em `dist/gtm.html`, e cola-se tal como está:
111
- só se trocam a chave e os hosts do checkout.
121
+ só se trocam a chave, o `apiHost` (se a account tiver o proxy gerido, abaixo) e
122
+ os hosts do checkout.
112
123
 
113
124
  ```html
114
125
  <script>
115
126
  window.f10xTracking = {
116
127
  key: '{{PostHog project key}}',
128
+ apiHost: '',
117
129
  checkoutHosts: []
118
130
  }
119
131
  </script>
120
- <script src="https://cdn.jsdelivr.net/npm/@funnel10x/tracking@1.0.0/dist/tag.js" integrity="sha384-…" crossorigin="anonymous"></script>
132
+ <script src="https://cdn.jsdelivr.net/npm/@funnel10x/tracking@1.4.0/dist/tag.js" integrity="sha384-…" crossorigin="anonymous"></script>
121
133
  ```
122
134
 
123
- Sem `site.json` não há interruptor: o snippet corre como a tag o deixar (as
124
- condições de consentimento da tag no GTM, se o site tiver um banner). Uma versão
125
- nova é uma tag nova, nunca `@1` nem `@latest`: o GTM de cada site diz que versão
126
- corre, e o `tracking_version` de cada evento confirma-o.
135
+ Uma versão nova é uma tag nova, nunca `@1` nem `@latest`: o GTM de cada site
136
+ diz que versão corre, e o `tracking_version` de cada evento confirma-o.
137
+
138
+ **O proxy gerido do PostHog, opcional.** Sem build não há o `/_t`, e com o
139
+ `apiHost` vazio os envios vão para o `us.i.posthog.com`: funciona, mas a
140
+ EasyPrivacy bloqueia-o, e a visita de quem tem um bloqueador não chega ao
141
+ PostHog. O PostHog tem um proxy
142
+ gerido, um subdomínio da account (um CNAME para o PostHog, nas definições da
143
+ organização, *Managed reverse proxy*) que recebe os envios e os passa ao
144
+ PostHog. Com ele, o `apiHost` da tag é esse subdomínio: na IR,
145
+ `https://p.infinityresponse.com`, e na IMO Circle, `https://p.imocircle.com`
146
+ (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;
147
+ o uBlock Origin no Firefox, que lê o CNAME, apanha na mesma, e quanto se
148
+ recupera não se mede, porque o que é bloqueado não chega para se contar. O
149
+ subdomínio é de outra origem que a página, e por isso o browser só lhe manda a
150
+ origem no `Referer`, e não o URL com a query string.
151
+
152
+ **O consentimento.** Sem `site.json` o snippet não tem interruptor, e quem
153
+ decide se ele corre é o sítio onde a tag está. No GTM, corre como as condições
154
+ de consentimento da tag deixarem (o banner ligado ao GTM); no *Head tracking
155
+ code* do GHL, ou no campo de código de outra plataforma, corre sempre, também
156
+ para quem recusou. Por isso, **num site com banner, a tag fica no GTM, com a
157
+ condição do consentimento de analytics; num site sem banner, pode ir para o
158
+ head**, o que a livra de um GTM bloqueado (a EasyPrivacy bloqueia o
159
+ `googletagmanager.com`). É uma regra de onde se põe a tag, e não código.
127
160
 
128
161
  **Numa página de um funil do GHL, o form é nativo** (sem `<form>`, cada campo
129
162
  com o query key em `data-q`), e **um campo escondido só se enche pelo URL com
@@ -145,24 +178,29 @@ ao form o que veio no URL dele.
145
178
 
146
179
  O que os forms, o iframe e os links do checkout levam:
147
180
 
148
- | campo | o que é | sem consentimento |
149
- | --- | --- | --- |
150
- | `ph_anon_id` | o id do dispositivo no PostHog (o anónimo de origem) | não |
151
- | `ph_distinct_id` | o distinct id agora (o id do contacto, depois do identify) | não |
152
- | `ph_session_id` | a sessão do PostHog | não |
153
- | `contact_id` | o id do contacto que veio num link desta sessão | não |
154
- | as UTMs | `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, `utm_id`, `utm_placement` | sim, do URL |
155
- | os click ids | `fbclid`, `gclid`, `gbraid`, `wbraid`, `gad_source`, `gad_campaignid`, `msclkid`, `ctwa_clid` | sim, do URL |
156
- | 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 |
157
- | `lp` | a página de entrada que o URL declara | sim, do URL |
181
+ | campo | o que é | sem consentimento | campo custom no GHL |
182
+ | --- | --- | --- | --- |
183
+ | `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 |
184
+ | `ph_distinct_id` | o distinct id agora (o id do contacto, depois do identify) | não | não |
185
+ | `ph_session_id` | a sessão do PostHog | não | não |
186
+ | `contact_id` | o id do contacto que veio num link desta sessão | não | não |
187
+ | 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 |
188
+ | os click ids | `fbclid`, `gclid`, `gbraid`, `wbraid`, `gad_source`, `gad_campaignid`, `msclkid`, `ctwa_clid` | sim, do URL | não |
189
+ | 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 |
190
+ | `lp` | a página de entrada que o URL declara | sim, do URL | não |
191
+ | `narrative` | o slug da narrativa declarado pela página (desde a 1.4.0) | sim, do URL | `narrative`, com o query key igual ao nome, se a location o tiver |
158
192
 
159
193
  - **Num form da página**, um campo que já existe com o nome recebe o valor se
160
194
  for escondido ou estiver vazio; os que faltam acrescentam-se escondidos, só
161
195
  num form de lead (com um campo de email ou de telefone) e nunca num com
162
196
  `data-f10x-skip`. Voltam a preencher-se no envio, com os valores desse
163
197
  momento.
164
- - **Num form do GHL**, os campos custom do contacto com estes nomes, que a A5
165
- lê.
198
+ - **Num form do GHL** não há campos a pôr: a ponte para o contacto é o email
199
+ que a pessoa envia (o `contact_captured`), e o click id do contacto é a
200
+ atribuição nativa do GHL, que a A5 lê. Os quatro campos de atribuição da
201
+ tabela só existem nas locations que recebam leads de um site `direct` com
202
+ `consent: true`: quem recusa não tem PostHog, e um contacto criado pelo
203
+ webhook não tem atribuição nativa (plano, A2 revista a 2026-10-07).
166
204
  - **Nos links do checkout** (os do Stripe e os hosts em `checkoutHosts`), os ids
167
205
  sobrepõem-se ao que o link traga, e os parâmetros do anúncio só entram se o
168
206
  link não tiver os seus. Num Payment Link do Stripe vai também o
@@ -175,6 +213,8 @@ O que os forms, o iframe e os links do checkout levam:
175
213
 
176
214
  `init(options)` (ou a tag) devolve, e põe em `window.f10xTracking`:
177
215
 
216
+ - `mode`: `tag` ou `direct`; `frame` num iframe, `preview` numa
217
+ pré-visualização, e `conflict` quando a página já tem outro PostHog;
178
218
  - `fields()`: os valores de cima, agora;
179
219
  - `grant()`, `deny()`, `consent()`: o interruptor de um site `direct`;
180
220
  - `onReady(fn)`: quando o PostHog estiver a medir;
@@ -184,24 +224,68 @@ O que os forms, o iframe e os links do checkout levam:
184
224
 
185
225
  ## Os modelos de parâmetros de URL
186
226
 
227
+ **Os nomes vão nas UTMs, os ids em três parâmetros nossos**, com o prefixo
228
+ `a_` (de *attribution*), como a HYROS tem o `h_`, e desde a 1.3.0. Um nome é
229
+ editável no gestor de anúncios e um rename não dá erro nenhum; o id nasce com
230
+ o anúncio. Os três querem dizer o mesmo em todas as plataformas, e a plataforma
231
+ diz-se no `utm_source`:
232
+
233
+ | nível | parâmetro | Meta | Google |
234
+ | --- | --- | --- | --- |
235
+ | a campanha | `a_campaign_id` | `{{campaign.id}}` | `{campaignid}` |
236
+ | o conjunto (o ad set da Meta, o ad group da Google) | `a_adset_id` | `{{adset.id}}` | `{adgroupid}` |
237
+ | o anúncio | `a_ad_id` | `{{ad.id}}` | `{creative}` |
238
+
239
+ Na HYROS, o `h_ad_id` é o anúncio em todas, mas o nível de cima muda de nome e
240
+ de nível com a plataforma: o `fbc_id` é o ad set na Meta, o `gc_id` é a
241
+ campanha na Google; na Meta a campanha não vai, e na Google o ad group também
242
+ não. O `h_ad_id` e o `fbc_id` continuam aceites, pelos anúncios da IMO Circle
243
+ que ainda os trazem (4.324 pageviews desde abril, medidos a 2026-10-08). Os
244
+ nomes sem prefixo da 1.0.0 à 1.2.0 (`ad_id`, `adset_id`, `campaign_id`,
245
+ `adgroup_id`) saíram: nenhum anúncio os usou. Nenhum URL das três accounts
246
+ trazia já um parâmetro `a_` (medido no mesmo dia).
247
+
248
+ **O texto de cada anúncio sai do gerador de links** da Marketing Ops (o módulo
249
+ URL Parameters, Fase M48 do plano da Marketing), que o compõe a partir da regra
250
+ da fonte na Traffic Sources, do funil e do stage: é aí que se gera, e os dois
251
+ modelos de baixo são a forma do que ele dá. O `utm_campaign` deixou de ser o nome
252
+ da campanha (que chega pelo `a_campaign_id`) e é o funil e o stage,
253
+ `<funil>_<stage>[_<temperatura>]`; o nome do ad set e o `utm_id` saíram, porque
254
+ os ids `a_` já os dizem.
255
+
187
256
  **Meta**, nos *URL parameters* de cada anúncio:
188
257
 
189
258
  ```
190
- 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}}
259
+ utm_source=meta&utm_medium=paid_social&utm_campaign=acq_f1_webinar_cold&utm_content={{ad.name}}&utm_placement={{placement}}&a_campaign_id={{campaign.id}}&a_adset_id={{adset.id}}&a_ad_id={{ad.id}}
191
260
  ```
192
261
 
193
262
  O nome do anúncio acaba no código do asset (decisão 10 do plano), e é por isso
194
- que vai no `utm_content`.
263
+ que vai no `utm_content`. O `utm_source` é `meta`, como na Traffic Sources e nos
264
+ anúncios das três accounts (45.819 pageviews com `fbclid` em 90 dias, medidos a
265
+ 2026-10-08); até à 1.2.0 este modelo dizia `facebook`, que nenhum usava.
195
266
 
196
- **Google**, no *Final URL suffix* da conta, com o auto-tagging ligado (o
197
- `gclid`) e dois parâmetros personalizados: `campaign` em cada campanha (o nome
198
- dela) e `asset` em cada anúncio (o código do asset, que num anúncio de pesquisa,
199
- num responsive display ou na Performance Max não chega pelo nome):
267
+ **Google**, no *Final URL suffix* da campanha (ou do ad group, ou do anúncio:
268
+ o `utm_campaign` é o do funil e do stage, e por isso não vai ao nível da conta),
269
+ com o auto-tagging ligado (o `gclid`) e um parâmetro personalizado, `asset`, em
270
+ cada anúncio (o código do asset, que num anúncio de pesquisa, num responsive
271
+ display ou na Performance Max não chega pelo nome):
200
272
 
201
273
  ```
202
- utm_source=google&utm_medium=cpc&utm_campaign={_campaign}&utm_term={keyword}&utm_content={_asset}&campaign_id={campaignid}&adgroup_id={adgroupid}&ad_id={creative}
274
+ utm_source=google&utm_medium=cpc&utm_campaign=acq_f1_webinar&utm_term={keyword}&utm_content={_asset}&a_campaign_id={campaignid}&a_adset_id={adgroupid}&a_ad_id={creative}
203
275
  ```
204
276
 
277
+ ## Os links orgânicos
278
+
279
+ Um link que não é anúncio (a bio do Instagram, a descrição de um vídeo, um
280
+ email, o WhatsApp, um parceiro) leva as UTMs da fonte dele, pelas regras da
281
+ tabela **Traffic Sources** do admin, que é nomenclatura partilhada por todas as
282
+ accounts. Sem elas a visita chega sem origem, e o referrer de um browser in-app
283
+ ou de um cliente de email vem quase sempre vazio, o que a conta como *direct*.
284
+ Este README não copia a tabela, que é a fonte; o gerador de links da Marketing
285
+ Ops (o módulo URL Parameters, Fase M48 do plano da Marketing) monta-os a partir
286
+ dela, junta-os a um URL que já tenha parâmetros sem duplicar nenhum, e guarda
287
+ cada link com o sítio onde foi posto.
288
+
205
289
  ## O checklist de um funil
206
290
 
207
291
  Antes de ligar o tráfego:
@@ -209,18 +293,29 @@ Antes de ligar o tráfego:
209
293
  1. **O snippet carrega pela versão**: num repo `direct`, a versão exacta no
210
294
  `package.json`, a chave na variável de ambiente e os rewrites do `/_t` no
211
295
  `vercel.json`; num site de fora, a tag do `dist/gtm.html` da versão, com a
212
- chave da account, no GTM.
213
- 2. **O consentimento**: o `consent` do `site.json` escrito, e o banner a chamar
214
- `grant()` e `deny()`; num site de fora, as condições da tag no GTM.
215
- 3. **Os forms**: cada form do GHL com os campos escondidos da tabela, com o
216
- *query key* igual ao nome; o workflow do identify no envio; e um envio de
217
- teste com `?utm_source=teste&gclid=teste` deixa os campos preenchidos no
218
- contacto.
219
- 4. **Os links de email e de SMS** levam `contact_id={{contact.id}}`.
220
- 5. **O checkout**: o host em `checkoutHosts` (o Stripe já lá está), e num
296
+ chave da account e, se ela tiver o proxy gerido, o `apiHost`.
297
+ 2. **Nenhum outro PostHog na página**: a tag ou o snippet do PostHog que o site
298
+ já tenha (no GTM, no head, no código do site) sai quando este entra. Este
299
+ snippet já é o PostHog, e com outro na página não arranca (avisa na
300
+ consola, e o `window.f10xTracking.mode` fica `conflict`). Com a mesma chave
301
+ não se perde nada: quem volta mantém o id anónimo, e o URL inicial que o
302
+ outro deixou no cookie reescreve-se limpo. **O `apiHost` da tag nova é o
303
+ `api_host` que a antiga tinha** (o proxy, se ela o usava): vazio, perdia
304
+ quem hoje chega por ele.
305
+ 3. **O consentimento**: o `consent` do `site.json` escrito, e o banner a chamar
306
+ `grant()` e `deny()`; num site de fora, a tag no GTM com a condição do
307
+ consentimento se o site tiver banner, e no head só se não tiver.
308
+ 4. **Os forms**: um envio de teste, escrito à mão numa página aberta com
309
+ `?utm_source=teste`, deixa no PostHog um `contact_captured` com o email,
310
+ no momento do envio, e o contacto no GHL; um email escrito e não enviado não
311
+ deixa nada. Num site `direct` com consentimento, a location tem os quatro
312
+ campos de atribuição da tabela, com o *query key* igual ao nome.
313
+ 5. **Os links de email e de SMS** levam `contact_id={{contact.id}}`.
314
+ 6. **O checkout**: o host em `checkoutHosts` (o Stripe já lá está), e num
221
315
  checkout em código os `fields()` na metadata.
222
- 6. **Os anúncios**: os parâmetros de cima na Meta e na Google.
223
- 7. **A prova**: um pageview aberto com `?email=teste@example.com&utm_source=teste`
316
+ 7. **Os anúncios**: o texto do gerador de links nos anúncios da Meta e da
317
+ Google, e os links orgânicos gerados por ele, com as UTMs da Traffic Sources.
318
+ 8. **A prova**: um pageview aberto com `?email=teste@example.com&utm_source=teste`
224
319
  chega ao PostHog (Activity, no projeto da account) com a `utm_source` e sem
225
320
  o `email`, no evento e no perfil, e com o `tracking_version` da versão.
226
321
 
@@ -238,10 +333,42 @@ admin, em `tests/api/tracking.test.mjs` (a limpeza, sem browser) e
238
333
 
239
334
  Uma versão nova: subir a `version` no `package.json` (semver: uma mudança no
240
335
  que os forms ou os eventos levam é minor, uma que parte uma integração é
241
- major), `npm run build`, PR, e depois do merge, da `main`:
336
+ major), `npm run build`, PR, e depois do merge, da `main`, nesta pasta:
337
+
338
+ ```bash
339
+ npm publish --dry-run
340
+ ```
242
341
 
243
342
  ```bash
244
343
  npm publish
245
344
  ```
246
345
 
346
+ O primeiro confere a versão e os ficheiros sem publicar nada. **O segundo é do
347
+ dono**: a conta do npm (`funnel10x-org`) tem 2FA, e o `npm publish` pede uma
348
+ autenticação no browser (*EOTP: This operation requires a one-time
349
+ password*) mesmo com a sessão iniciada (medido a 2026-10-08, na 1.3.0). Um
350
+ agente prepara a pasta na `main` e corre o primeiro, e o dono corre o segundo
351
+ no terminal dele.
352
+
353
+ Depois de publicar, o npm avisa que a versão *may take a few minutes to become
354
+ available*: espera-se, e só então se confirma. O `npm view` lê de uma cache e
355
+ mostrou a versão anterior durante esses minutos; o registo, lido directamente,
356
+ diz a verdade:
357
+
358
+ ```bash
359
+ curl -s https://registry.npmjs.org/@funnel10x%2ftracking | node -e "process.stdin.on('data',d=>b+=d);let b='';process.stdin.on('end',()=>console.log(JSON.parse(b)['dist-tags']))"
360
+ ```
361
+
362
+ E o `tag.js` que o jsDelivr serve tem de ter o hash do `integrity` da tag do
363
+ GTM, senão a tag não carrega em site nenhum. As duas linhas têm de dar o mesmo
364
+ `sha384-…`:
365
+
366
+ ```bash
367
+ echo "sha384-$(curl -sL "https://cdn.jsdelivr.net/npm/@funnel10x/tracking@$(node -p "require('./package.json').version")/dist/tag.js" | openssl dgst -sha384 -binary | openssl base64 -A)"
368
+ ```
369
+
370
+ ```bash
371
+ grep -o 'sha384-[^"]*' dist/gtm.html
372
+ ```
373
+
247
374
  O `dist/THIRD_PARTY_LICENSES.txt` leva as licenças do que vai embutido.
package/dist/gtm.html CHANGED
@@ -1,8 +1,9 @@
1
- <!-- @funnel10x/tracking@1.2.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.4.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.2.0/dist/tag.js" integrity="sha384-HBEVk3iHklebWrwCgMWKmqvpecTCfdlKHwUh1ic9eiqkpraFHWtDpGguHg+5mxw/" crossorigin="anonymous"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/@funnel10x/tracking@1.4.0/dist/tag.js" integrity="sha384-UTBEFP06SUs6EqWR2bkYb7Rq6QEj2SWvMGpf1/3g4uhyNGYMzyucjjeHtYYoW/xD" 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