@funnel10x/tracking 1.3.0 → 2.0.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
@@ -5,22 +5,29 @@ atribuição](https://github.com/funnel10x/f10x-admin/blob/main/docs/plano-atrib
5
5
  do admin. Vive em `packages/tracking/` do `funnel10x/f10x-admin`, e publica-se
6
6
  no npm como pacote público, pela versão.
7
7
 
8
- O que faz numa página:
9
-
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
13
- caminho e só os parâmetros de campanha da allow-list (`src/clean.js`), no
14
- evento, no perfil (`$set`, `$set_once`), no cookie do PostHog, nos heatmaps
15
- e na gravação de sessão (onde os pedidos de rede se gravam sem o corpo). O
16
- email, o nome, o telefone e as respostas de um quiz que um funil
17
- passe na query string ficam no browser;
18
- - **o FingerprintJS v5** (MIT), sem o pedido de monitorização que ele faz por
19
- omissão a um servidor deles; o `visitorId` vai em cada evento (`fingerprint`);
20
- - **os ids e os parâmetros do anúncio nos forms**: nos campos escondidos de
21
- cada form de lead da página (os que já existem com o nome, e os que faltam,
8
+ **Desde a 2.0.0 (Fase A2b, 2026-10-14) o snippet é o PostHog oficial com os
9
+ nossos extras por cima**: o mesmo `array.js` que a tag oficial carrega, do mesmo
10
+ CDN, no canal 1.x, e o SDK não vem neste ficheiro. O que faz numa página:
11
+
12
+ - **o PostHog oficial**, por um loader: o stub (o `window.posthog` com o
13
+ `__SV`, como o do snippet oficial) posto antes de tudo, e o `array.js` do CDN
14
+ do PostHog no canal 1.x (`/static/1/array.js`, pelo host dos assets do
15
+ `apiHost`, ou pelo `/_t` de um repo `direct`). As funcionalidades ficam como
16
+ na instalação oficial (feature flags, surveys, conversations, product tours;
17
+ os web experiments desligados, como lá), e um site desliga uma pelas
18
+ `posthog` das opções. **O PostHog recebe tudo**: o snippet não limpa nada, e o
19
+ URL chega inteiro, com o que a query string traga (o email, as respostas de
20
+ um quiz, um `token`). Quem apara é o admin, na ingestão (abaixo);
21
+ - **o FingerprintJS v5** (MIT, embutido), sem o pedido de monitorização que ele
22
+ faz por omissão a um servidor deles. O primeiro `$pageview` espera por ele
23
+ até 1 s, contado com o carregamento dele, e cada evento leva o `fingerprint`
24
+ e o `fingerprint_ms` (o tempo que levou, ou o tecto, 1000);
25
+ - **a lista de atribuição nos forms, no CRM e no checkout, e só ela**: os ids
26
+ e os 22 parâmetros do anúncio (`src/params.js`) nos campos escondidos de cada
27
+ form de lead da página (os que já existem com o nome, e os que faltam,
22
28
  acrescentados), no URL do iframe de um form, inquérito ou calendário do GHL,
23
- e nos links do checkout;
29
+ e nos links do checkout. O email, o `token` e o resto do URL não seguem: iam
30
+ para o URL do Stripe e para os campos do GHL, que é entregá-los a terceiros;
24
31
  - **o identify pelo id do contacto** num link de email ou de SMS
25
32
  (`?contact_id={{contact.id}}`);
26
33
  - **o email e o telefone que a pessoa envia** (desde a 1.1.0, como a HYROS;
@@ -28,7 +35,7 @@ O que faz numa página:
28
35
  (`email`, `phone`, `phoneNumber`, `phone_number`, `tel`, `telefone`), e dos
29
36
  campos de email e de telefone do form que ela envia (o `submit` de um
30
37
  `<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
38
+ não enviou não vai. O telefone
32
39
  ganha o indicativo do seletor de país (o intl-tel-input), que é o que um
33
40
  seletor costuma esconder. Vão num evento `contact_captured` (com
34
41
  `contact_source`: `url` ou `form`), cada valor uma vez por página, e
@@ -40,19 +47,89 @@ O que faz numa página:
40
47
  **interruptor de consentimento** do `site.json`, e a guarda das
41
48
  pré-visualizações.
42
49
 
43
- O posthog-js e o FingerprintJS vão dentro do ficheiro, pela versão que este
44
- pacote fixa, e não se resolvem no site. O que fica de fora são as extensões que
45
- o posthog-js carrega quando o projeto as liga (o gravador de sessão, os dead
46
- clicks, os web vitals), que vêm do PostHog pela mesma versão do posthog-js
47
- (`/static/<versão>/…`, pelo proxy num repo `direct`): esse código não passa
48
- pelo `integrity` da tag.
50
+ **Quem lê isto do lado do admin** (Fase A3 do plano da atribuição): a fonte
51
+ PostHog da ingestão guarda de cada toque o `tracking_version`, a
52
+ `page_version`, o `ab_test`, o `fingerprint`, e desde a A2b a versão do SDK e a
53
+ data dos `defaults` (`$lib_version` e `$config_defaults`), e o email e o
54
+ telefone do `contact_captured` só em hash. **Apara cada URL pela mesma lista**
55
+ de `src/params.js`: um parâmetro que entre na `ALLOWED_PARAMS` entra também na
56
+ entrada `posthog` da `ingest/pii.json` e na `supabase/verify/atr3.sql` do
57
+ admin, no mesmo PR (um teste do admin prova que as três listas são a mesma).
58
+
59
+ ### Uma de duas instalações por site
60
+
61
+ Cada site, de qualquer account, escolhe **uma** de duas instalações, e nunca as
62
+ duas na mesma página (decisão 1 da A2b): a **oficial do PostHog** (a tag no
63
+ GTM, o snippet oficial no código do site, ou o posthog-js no código de uma app)
64
+ ou **este snippet** (embutido num repo `direct`, ou pela tag do
65
+ `dist/gtm.html` em qualquer outro site). Quem escolhe este não perde nada da
66
+ oficial: é o mesmo SDK, com os extras. Com as duas, a segunda ou toma o
67
+ arranque da primeira ou duplica cada evento:
68
+
69
+ - **a oficial antes deste**: o primeiro `init` da fila manda, e este não
70
+ arranca; avisa na consola (*another PostHog is already on this page*) e o
71
+ `window.f10xTracking.mode` fica `conflict`;
72
+ - **a oficial depois deste**: o stub deste é o `window.posthog`, e o `init`
73
+ dela entra na fila dele, sem efeito (o stub nunca insere um script, por isso
74
+ não há outro `array.js`); avisa na consola, e cada evento leva
75
+ `tracking_conflict: true`, que é o sinal nos dados. A
76
+ `supabase/verify/atr2b.sql` do admin guarda o resto: num host com este
77
+ snippet, nenhum `$pageview` de outra instalação no mesmo minuto.
78
+
79
+ Num contentor GTM partilhado por vários sites, a tag oficial dispara só nos
80
+ hosts que a escolheram (uma excepção por hostname no trigger). **Dentro de um
81
+ domínio registável, todas as instalações levam a mesma data de `defaults`**: o
82
+ cookie do PostHog é partilhado pelos subdomínios, e a data muda a regra entre o
83
+ cookie e o `localStorage` (`cookieWinsOnConflict`). A deste pacote é
84
+ `2026-08-30`; uma tag oficial no mesmo domínio passa a ela.
85
+
86
+ ### O que se actualiza sozinho, e o que não
87
+
88
+ | camada | quem a dá | actualiza-se |
89
+ | --- | --- | --- |
90
+ | o SDK (`array.js`) | o CDN do PostHog, no canal 1.x | **sozinho**, como na tag oficial: o canal tem 4 horas de cache, e uma versão nova chega a todos os sites nesse prazo |
91
+ | as extensões (o gravador, os surveys) | o mesmo CDN | seguem a versão do `array.js` (`strict_script_versioning: true`) |
92
+ | o comportamento por omissão do SDK | a data dos `defaults`, fixa neste pacote | só com uma versão nova do pacote |
93
+ | os extras (este ficheiro) | o jsDelivr pela versão, com o `integrity`, numa tag; o `package.json`, num `direct` | **nunca sozinhos**: decidem o que vai para os forms, o CRM e o checkout, e uma mudança não anunciada chegaria a todos os sites ao mesmo tempo. Num `direct`, uma versão nova do pacote é uma release da base |
94
+
95
+ Os testes A/B de um site `direct` correm sobre um SDK externo: uma versão nova
96
+ chega a todas as versões de um teste ao mesmo tempo, e a comparação entre elas
97
+ continua válida. O admin guarda a versão do SDK e a data dos `defaults` de cada
98
+ toque, para uma análise se poder partir por elas.
99
+
100
+ **O `sdkVersion`, numa emergência.** Uma versão exacta do posthog-js
101
+ (`'1.438.9'`) fixa o caminho exacto do `array.js`, que é imutável no CDN: numa
102
+ tag, na configuração dela; num repo `direct`, pela variável de ambiente da
103
+ Vercel (`VITE_POSTHOG_SDK_VERSION` num `static`, `NEXT_PUBLIC_POSTHOG_SDK_VERSION`
104
+ num `nextjs`), que muda com um redeploy, sem PR. É a resposta a uma versão do
105
+ SDK que parta um site, até uma versão nova do pacote; o workflow diário
106
+ `tracking-sdk.yml` do admin corre os testes contra o `array.js` que o CDN serve
107
+ nesse dia, e abre uma issue quando falham.
108
+
109
+ ### O que muda por o PostHog receber tudo
110
+
111
+ O que a 1.x limpava no browser vai inteiro ao PostHog (decisão 2 da A2b), e
112
+ isso tem três consequências que se sabem:
113
+
114
+ - **o cookie do PostHog leva o URL de entrada em bruto** (o
115
+ `$initial_person_info`), e o cookie é partilhado por todos os subdomínios do
116
+ domínio registável;
117
+ - **as gravações seguem as definições do projeto**, com os corpos dos pedidos
118
+ de rede se o projeto os ligar (um form de lead manda o email);
119
+ - **o `Referer` de um pedido ao `/_t`** (do mesmo domínio) leva o URL inteiro,
120
+ e o site volta à política do browser (`strict-origin-when-cross-origin`): o
121
+ `strict-origin` existia por isso, e a razão caiu.
122
+
123
+ **Um site com CSP** tem de deixar passar o host dos assets do PostHog no
124
+ `script-src` (o `us-assets.i.posthog.com`, ou o proxy da account): o SDK deixou
125
+ de vir no ficheiro. É uma das razões de a 2.0.0 ser major.
49
126
 
50
127
  ## Num repo `direct`: embutido no build
51
128
 
52
129
  O `package.json` do repo pina a versão exacta (sem `^`), e o build embute-o:
53
130
 
54
131
  ```json
55
- { "dependencies": { "@funnel10x/tracking": "1.0.0" } }
132
+ { "dependencies": { "@funnel10x/tracking": "2.0.0" } }
56
133
  ```
57
134
 
58
135
  ```js
@@ -61,18 +138,20 @@ import { init } from '@funnel10x/tracking'
61
138
  init({
62
139
  key: import.meta.env.VITE_POSTHOG_KEY,
63
140
  apiHost: '/_t',
141
+ sdkVersion: import.meta.env.VITE_POSTHOG_SDK_VERSION || undefined,
64
142
  consent: site.consent,
65
143
  pageVersion: 3,
66
- test: '/pt/f1/lp#2',
144
+ test: '/pt/acq_f1/webinar/lp#2',
67
145
  })
68
146
  ```
69
147
 
70
148
  - `key`: a chave do projeto PostHog da account (`phc_…`), pela variável de
71
149
  ambiente da Vercel (`VITE_POSTHOG_KEY` num `static`, `NEXT_PUBLIC_POSTHOG_KEY`
72
150
  num `nextjs`). É pública: vai no HTML de qualquer maneira.
73
- - `apiHost: '/_t'`: os eventos vão pelo **proxy no domínio do site**, que livra
74
- os envios das listas de bloqueio (a EasyPrivacy bloqueia o `posthog.com`). O
75
- `vercel.json` do repo leva os três rewrites, por esta ordem:
151
+ - `apiHost: '/_t'`: o SDK e os eventos vão pelo **proxy no domínio do site**,
152
+ que os livra das listas de bloqueio (a EasyPrivacy bloqueia o `posthog.com`):
153
+ o `array.js` pede-se a `/_t/static/1/array.js`. O `vercel.json` do repo leva
154
+ os três rewrites, por esta ordem:
76
155
 
77
156
  ```json
78
157
  {
@@ -87,16 +166,25 @@ init({
87
166
  O padrão é `:path(.*)` e não `:path*`, que não apanha a barra final dos
88
167
  envios (`/e/`) e lhes responde 404 (medido em produção). O `_` à frente
89
168
  garante que não colide com um funil (a base não deixa um nome
90
- de funil começar por `_`). **E o site leva `Referrer-Policy: strict-origin`**
91
- (nos `headers` do `vercel.json`): o proxy é do mesmo domínio, e com a política
92
- por omissão o browser manda-lhe o URL inteiro da página no `Referer`, com o
93
- que a query string traga, e o rewrite reencaminha-o para o PostHog.
169
+ de funil começar por `_`). Até à 1.x o site levava `Referrer-Policy:
170
+ strict-origin`, para o `Referer` de um pedido ao proxy não levar o URL
171
+ inteiro; desde a 2.0.0 o PostHog recebe-o de qualquer maneira, e a base volta
172
+ à política do browser.
173
+ - `sdkVersion`: vazio, o canal 1.x do CDN; uma versão exacta só numa
174
+ emergência (acima), pela variável de ambiente da Vercel
175
+ (`VITE_POSTHOG_SDK_VERSION` num `static`, `NEXT_PUBLIC_POSTHOG_SDK_VERSION` num
176
+ `nextjs`), que o `_scripts/tracking.mjs` e o `Tracking.tsx` da base lêem.
94
177
  - `consent`: o do `site.json`. Com `true`, o snippet não mede nem guarda nada
95
178
  até à escolha: os forms levam só os parâmetros do anúncio tirados do URL, e o
96
179
  banner do site chama `window.f10xTracking.grant()` no sim e `.deny()` no não
97
- (ou para retirar o sim, que pára o PostHog e apaga o que ele guardou). A
98
- escolha guarda-se em `localStorage` (`f10x_consent`), e a página seguinte
99
- arranca sozinha. Com `false`, mede logo.
180
+ (ou para retirar o sim, que pára o PostHog e apaga o que ele guardou). **Antes
181
+ do sim nada se pede, nem o `array.js`**: o padrão que o PostHog recomenda,
182
+ carregar com a recolha desligada, pede a configuração remota antes da
183
+ escolha. Um não antes de o SDK chegar cancela o arranque (o `init` sai da
184
+ fila, e o `array.js` não se pede). A escolha guarda-se em `localStorage`
185
+ (`f10x_consent`), e a página seguinte arranca sozinha. Com `false`, mede
186
+ logo. **O GTM do site arranca depois do stub do snippet**, para uma tag que
187
+ use o `window.posthog` o encontrar.
100
188
  - `pageVersion` e `test`: o `version` da página principal no `tests.json` (ou o
101
189
  `N` de uma variante) e o nome do teste activo, que o build entrega.
102
190
 
@@ -105,33 +193,39 @@ de uma versão) não mede nada nem mexe nos forms.
105
193
 
106
194
  ## Num site que não é repo nosso: a tag do GTM
107
195
 
108
- Num funil do GHL ou num site em Webflow, onde não há build, o mesmo pacote
109
- carrega-se por uma tag Custom HTML do GTM (em All Pages), **pela versão fixa**
110
- no URL do jsDelivr e com o hash do ficheiro no `integrity`. A tag de cada versão
111
- publicada está no próprio pacote, em `dist/gtm.html`, e cola-se tal como está:
112
- só se trocam a chave, o `apiHost` (se a account tiver o proxy gerido, abaixo) e
113
- os hosts do checkout.
196
+ Num funil do GHL, num site institucional ou num site em Webflow, onde não há
197
+ build, o mesmo pacote carrega-se por uma tag Custom HTML do GTM (em All Pages),
198
+ **em vez da tag oficial do PostHog, e nunca ao lado dela** (*Uma de duas
199
+ instalações por site*, acima), **pela versão fixa** no URL do jsDelivr e com o
200
+ hash do ficheiro no `integrity`. A tag de cada versão publicada está no próprio
201
+ pacote, em `dist/gtm.html`, e cola-se tal como está: só se trocam a chave, o
202
+ `apiHost` (se a account tiver o proxy gerido, abaixo), os hosts do checkout e,
203
+ numa emergência, o `sdkVersion`.
114
204
 
115
205
  ```html
116
206
  <script>
117
207
  window.f10xTracking = {
118
208
  key: '{{PostHog project key}}',
119
209
  apiHost: '',
210
+ sdkVersion: '',
120
211
  checkoutHosts: []
121
212
  }
122
213
  </script>
123
- <script src="https://cdn.jsdelivr.net/npm/@funnel10x/tracking@1.3.0/dist/tag.js" integrity="sha384-…" crossorigin="anonymous"></script>
214
+ <script src="https://cdn.jsdelivr.net/npm/@funnel10x/tracking@2.0.0/dist/tag.js" integrity="sha384-…" crossorigin="anonymous"></script>
124
215
  ```
125
216
 
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.
217
+ Uma versão nova é uma tag nova, nunca `@2` nem `@latest`: o GTM de cada site
218
+ diz que versão dos extras corre, e o `tracking_version` de cada evento
219
+ confirma-o. O SDK, esse, actualiza-se sozinho, pelo CDN do PostHog.
128
220
 
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
221
+ **O proxy gerido do PostHog, opcional.** Sem build não há o `/_t`, e com o
222
+ `apiHost` vazio os envios vão para o `us.i.posthog.com`: funciona, mas a
223
+ EasyPrivacy bloqueia-o, e a visita de quem tem um bloqueador não chega ao
224
+ PostHog. O PostHog tem um proxy
132
225
  gerido, um subdomínio da account (um CNAME para o PostHog, nas definições da
133
226
  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,
227
+ PostHog, e serve também o `array.js`. Com ele, o `apiHost` da tag é esse
228
+ subdomínio: na IR,
135
229
  `https://p.infinityresponse.com`, e na IMO Circle, `https://p.imocircle.com`
136
230
  (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
231
  o uBlock Origin no Firefox, que lê o CNAME, apanha na mesma, e quanto se
@@ -178,6 +272,7 @@ O que os forms, o iframe e os links do checkout levam:
178
272
  | os click ids | `fbclid`, `gclid`, `gbraid`, `wbraid`, `gad_source`, `gad_campaignid`, `msclkid`, `ctwa_clid` | sim, do URL | não |
179
273
  | 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
274
  | `lp` | a página de entrada que o URL declara | sim, do URL | não |
275
+ | `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 |
181
276
 
182
277
  - **Num form da página**, um campo que já existe com o nome recebe o valor se
183
278
  for escondido ou estiver vazio; os que faltam acrescentam-se escondidos, só
@@ -197,19 +292,27 @@ O que os forms, o iframe e os links do checkout levam:
197
292
  checkout.
198
293
  - **Num checkout criado no servidor** (uma sessão do Stripe em código),
199
294
  `window.f10xTracking.fields()` dá os mesmos valores, para a metadata.
295
+ Os `ph_distinct_id`, `ph_session_id`, `ph_anon_id` e `contact_id` chegam ao
296
+ admin pela A4, com forma de id. A `sale_checkout_keys` expõe-os, e a
297
+ `sale_posthog_links` liga a venda pelo `cs_…` observado no PostHog primeiro,
298
+ senão pelo primeiro id da metadata que o projeto da account tenha visto.
200
299
 
201
300
  ## A API
202
301
 
203
302
  `init(options)` (ou a tag) devolve, e põe em `window.f10xTracking`:
204
303
 
205
304
  - `mode`: `tag` ou `direct`; `frame` num iframe, `preview` numa
206
- pré-visualização, e `conflict` quando a página já tem outro PostHog;
305
+ pré-visualização, e `conflict` quando a página já tinha outro PostHog antes
306
+ deste (um que chegue depois não muda o `mode`: deixa `tracking_conflict` nos
307
+ eventos);
207
308
  - `fields()`: os valores de cima, agora;
208
309
  - `grant()`, `deny()`, `consent()`: o interruptor de um site `direct`;
209
310
  - `onReady(fn)`: quando o PostHog estiver a medir;
210
311
  - `capture(event, properties)`: um evento pelo mesmo PostHog (o `lead` de um
211
312
  form em código), que sem consentimento não faz nada;
212
- - `posthog`: a instância, para o que não está aqui.
313
+ - `posthog`: a instância do PostHog depois de o SDK arrancar, e o stub até lá
314
+ (o que se lhe chamar fica na fila, como no stub oficial). O
315
+ `window.posthog` é o mesmo, e é por ele que as tags do GTM o usam.
213
316
 
214
317
  ## Os modelos de parâmetros de URL
215
318
 
@@ -234,26 +337,45 @@ nomes sem prefixo da 1.0.0 à 1.2.0 (`ad_id`, `adset_id`, `campaign_id`,
234
337
  `adgroup_id`) saíram: nenhum anúncio os usou. Nenhum URL das três accounts
235
338
  trazia já um parâmetro `a_` (medido no mesmo dia).
236
339
 
340
+ **O texto de cada anúncio sai do gerador de links** da Marketing Ops (o módulo
341
+ URL Parameters, Fase M48 do plano da Marketing), que o compõe a partir da regra
342
+ da fonte na Traffic Sources, do funil e do stage: é aí que se gera, e os dois
343
+ modelos de baixo são a forma do que ele dá. O `utm_campaign` deixou de ser o nome
344
+ da campanha (que chega pelo `a_campaign_id`) e é o funil e o stage,
345
+ `<funil>_<stage>[_<temperatura>]`; o nome do ad set e o `utm_id` saíram, porque
346
+ os ids `a_` já os dizem.
347
+
237
348
  **Meta**, nos *URL parameters* de cada anúncio:
238
349
 
239
350
  ```
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}}
351
+ utm_source=meta&utm_medium=paid_social&utm_campaign=acq_f1_webinar_cold&utm_term={{campaign.name}} >> {{adset.name}}&utm_content={{ad.name}}&utm_placement={{placement}}&a_campaign_id={{campaign.id}}&a_adset_id={{adset.id}}&a_ad_id={{ad.id}}
241
352
  ```
242
353
 
243
354
  O nome do anúncio acaba no código do asset (decisão 10 do plano), e é por isso
244
- que vai no `utm_content`. O `utm_source` é `meta`, como na Traffic Sources e nos
355
+ que vai no `utm_content`; o `utm_term` leva a campanha e o ad set, separados por
356
+ ` >> ` (Fase M60 do plano da Marketing), só para se ler no PostHog: nenhum
357
+ código o lê, e quem liga o clique ao anúncio são os ids `a_`. O `utm_source` é `meta`, como na Traffic Sources e nos
245
358
  anúncios das três accounts (45.819 pageviews com `fbclid` em 90 dias, medidos a
246
359
  2026-10-08); até à 1.2.0 este modelo dizia `facebook`, que nenhum usava.
247
360
 
248
- **Google**, no *Final URL suffix* da conta, com o auto-tagging ligado (o
249
- `gclid`) e dois parâmetros personalizados: `campaign` em cada campanha (o nome
250
- dela) e `asset` em cada anúncio (o código do asset, que num anúncio de pesquisa,
251
- num responsive display ou na Performance Max não chega pelo nome):
361
+ **Google**, no *Final URL suffix* da campanha (ou do ad group, ou do anúncio:
362
+ o `utm_campaign` é o do funil e do stage, e por isso não vai ao nível da conta),
363
+ com o auto-tagging ligado (o `gclid`). A Google não tem macro para nomes, só
364
+ para ids, e por isso os nomes vão por três parâmetros personalizados, escritos à
365
+ mão, com o nome só de letras e dígitos e a referência com o `_` à frente:
366
+ `campaign` (`{_campaign}`) na campanha, `adgroup` (`{_adgroup}`) em cada ad
367
+ group e `ad` (`{_ad}`) em cada anúncio (o nome dele, que acaba no código do
368
+ asset; num anúncio de pesquisa, que não tem campo de nome, é o único nome que
369
+ leva). Sem eles chegam os ids `a_`, que
370
+ são a garantia. Numa campanha de pesquisa o `utm_term` leva a keyword no fim, e
371
+ o ` >> ` vai codificado:
252
372
 
253
373
  ```
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}
374
+ utm_source=google&utm_medium=cpc&utm_campaign=acq_f1_webinar&utm_term={_campaign}%20%3E%3E%20{_adgroup}%20%3E%3E%20{keyword}&utm_content={_ad}&a_campaign_id={campaignid}&a_adset_id={adgroupid}&a_ad_id={creative}
255
375
  ```
256
376
 
377
+ Fora da pesquisa, o mesmo sem o `%20%3E%3E%20{keyword}`.
378
+
257
379
  ## Os links orgânicos
258
380
 
259
381
  Um link que não é anúncio (a bio do Instagram, a descrição de um vídeo, um
@@ -262,22 +384,27 @@ tabela **Traffic Sources** do admin, que é nomenclatura partilhada por todas as
262
384
  accounts. Sem elas a visita chega sem origem, e o referrer de um browser in-app
263
385
  ou de um cliente de email vem quase sempre vazio, o que a conta como *direct*.
264
386
  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).
387
+ Ops (o módulo URL Parameters, Fase M48 do plano da Marketing) monta-os a partir
388
+ dela, junta-os a um URL que já tenha parâmetros sem duplicar nenhum, e guarda
389
+ cada link com o sítio onde foi posto.
266
390
 
267
391
  ## O checklist de um funil
268
392
 
269
393
  Antes de ligar o tráfego:
270
394
 
271
395
  1. **O snippet carrega pela versão**: num repo `direct`, a versão exacta no
272
- `package.json`, a chave na variável de ambiente e os rewrites do `/_t` no
273
- `vercel.json`; num site de fora, a tag do `dist/gtm.html` da versão, com a
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.
396
+ `package.json` (a da base), a chave na variável de ambiente e os rewrites
397
+ do `/_t` no `vercel.json`; num site de fora, a tag do `dist/gtm.html` da
398
+ versão, com a chave da account e, se ela tiver o proxy gerido, o `apiHost`.
399
+ O `sdkVersion` fica vazio.
400
+ 2. **Uma só instalação na página**: este snippet ou a oficial do PostHog,
401
+ nunca as duas. Ao escolher este, a tag ou o snippet do PostHog que o site já
402
+ tenha (no GTM, no head, no código do site) sai quando este entra, e num
403
+ contentor partilhado a tag oficial ganha a excepção do hostname. Com a mesma
404
+ chave não se perde nada: quem volta mantém o id anónimo. **O `apiHost` da
405
+ tag nova é o `api_host` que a antiga tinha** (o proxy, se ela o usava):
406
+ vazio, perdia quem hoje chega por ele. **A data dos `defaults` é a mesma em
407
+ todas as instalações do domínio registável** (a deste pacote).
281
408
  3. **O consentimento**: o `consent` do `site.json` escrito, e o banner a chamar
282
409
  `grant()` e `deny()`; num site de fora, a tag no GTM com a condição do
283
410
  consentimento se o site tiver banner, e no head só se não tiver.
@@ -289,11 +416,14 @@ Antes de ligar o tráfego:
289
416
  5. **Os links de email e de SMS** levam `contact_id={{contact.id}}`.
290
417
  6. **O checkout**: o host em `checkoutHosts` (o Stripe já lá está), e num
291
418
  checkout em código os `fields()` na metadata.
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`
295
- chega ao PostHog (Activity, no projeto da account) com a `utm_source` e sem
296
- o `email`, no evento e no perfil, e com o `tracking_version` da versão.
419
+ 7. **Os anúncios**: o texto do gerador de links nos anúncios da Meta e da
420
+ Google, e os links orgânicos gerados por ele, com as UTMs da Traffic Sources.
421
+ 8. **A prova**: na consola, `window.f10xTracking.mode` não é `conflict`; um
422
+ pageview aberto com `?email=teste@example.com&utm_source=teste` chega ao
423
+ PostHog (Activity, no projeto da account) uma vez só, inteiro, com o
424
+ `tracking_version` da versão, o `$lib_version` que o CDN serve e nenhum
425
+ `tracking_conflict`; e os campos do form e o link do checkout levam a
426
+ `utm_source` e não o `email`.
297
427
 
298
428
  ## Desenvolver e publicar
299
429
 
@@ -304,15 +434,52 @@ npm run build
304
434
 
305
435
  O `dist/` commita-se, como o build das apps do admin, e a CI refaz-o e falha se
306
436
  não der os mesmos bytes: o que se publica é o que se testou. Os testes estão no
307
- admin, em `tests/api/tracking.test.mjs` (a limpeza, sem browser) e
308
- `tests/tracking.spec.mjs` (o snippet numa página, contra o `dist/`).
437
+ admin, em `tests/api/tracking.test.mjs` (as listas e o pacote, sem browser) e
438
+ `tests/tracking.spec.mjs` (o snippet numa página, contra o `dist/`, com o
439
+ `array.js` da devDependency servido sem rede). O workflow `tracking-sdk.yml`
440
+ corre o mesmo spec uma vez por dia contra o `array.js` que o CDN do PostHog
441
+ serve nesse dia, e abre uma issue quando falha.
309
442
 
310
443
  Uma versão nova: subir a `version` no `package.json` (semver: uma mudança no
311
444
  que os forms ou os eventos levam é minor, uma que parte uma integração é
312
- major), `npm run build`, PR, e depois do merge, da `main`:
445
+ major), `npm run build`, PR, e depois do merge, da `main`, nesta pasta:
446
+
447
+ ```bash
448
+ npm publish --dry-run
449
+ ```
313
450
 
314
451
  ```bash
315
452
  npm publish
316
453
  ```
317
454
 
455
+ O primeiro confere a versão e os ficheiros sem publicar nada. **O segundo é do
456
+ dono**: a conta do npm (`funnel10x-org`) tem 2FA, e o `npm publish` pede uma
457
+ autenticação no browser (*EOTP: This operation requires a one-time
458
+ password*) mesmo com a sessão iniciada (medido a 2026-10-08, na 1.3.0). Um
459
+ agente prepara a pasta na `main` e corre o primeiro, e o dono corre o segundo
460
+ no terminal dele. A alternativa sem OTP é o *trusted publishing* do npm (a
461
+ publicação pela CI do GitHub, sem token), que é configuração do dono no
462
+ npmjs.com.
463
+
464
+ Depois de publicar, o npm avisa que a versão *may take a few minutes to become
465
+ available*: espera-se, e só então se confirma. O `npm view` lê de uma cache e
466
+ mostrou a versão anterior durante esses minutos; o registo, lido directamente,
467
+ diz a verdade:
468
+
469
+ ```bash
470
+ 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']))"
471
+ ```
472
+
473
+ E o `tag.js` que o jsDelivr serve tem de ter o hash do `integrity` da tag do
474
+ GTM, senão a tag não carrega em site nenhum. As duas linhas têm de dar o mesmo
475
+ `sha384-…`:
476
+
477
+ ```bash
478
+ 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)"
479
+ ```
480
+
481
+ ```bash
482
+ grep -o 'sha384-[^"]*' dist/gtm.html
483
+ ```
484
+
318
485
  O `dist/THIRD_PARTY_LICENSES.txt` leva as licenças do que vai embutido.