@funnel10x/tracking 2.0.0 → 2.2.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
@@ -28,6 +28,9 @@ CDN, no canal 1.x, e o SDK não vem neste ficheiro. O que faz numa página:
28
28
  acrescentados), no URL do iframe de um form, inquérito ou calendário do GHL,
29
29
  e nos links do checkout. O email, o `token` e o resto do URL não seguem: iam
30
30
  para o URL do Stripe e para os campos do GHL, que é entregá-los a terceiros;
31
+ - **o `checkout` no clique num link do checkout** (desde a 2.2.0, Fase A41),
32
+ uma vez por link e por página: o início do checkout, do contrato dos
33
+ eventos (abaixo);
31
34
  - **o identify pelo id do contacto** num link de email ou de SMS
32
35
  (`?contact_id={{contact.id}}`);
33
36
  - **o email e o telefone que a pessoa envia** (desde a 1.1.0, como a HYROS;
@@ -45,26 +48,47 @@ CDN, no canal 1.x, e o SDK não vem neste ficheiro. O que faz numa página:
45
48
  - **a versão do snippet em cada evento** (`tracking_version`), e, num site
46
49
  `direct`, **a versão da página e o teste** (`page_version`, `ab_test`), o
47
50
  **interruptor de consentimento** do `site.json`, e a guarda das
48
- pré-visualizações.
51
+ pré-visualizações;
52
+ - **os extras isolados do arranque do SDK** (desde a 2.1.0, Fase A2c): os
53
+ forms, o iframe e o checkout (`dom`), o email e o telefone (`contact`), o
54
+ FingerprintJS (`fingerprint`) e o cross-domain (`linker`) correm cada um no
55
+ seu `try`. Um que lance deixa na consola *the dom extra failed* (com o nome
56
+ dele) e, em cada evento, o `tracking_degraded` com a lista dos que falharam
57
+ (`['dom']`), e o PostHog mede na mesma, com o `tracking_version`. No 2.0.0 um
58
+ erro em qualquer deles impedia o SDK de carregar. Um erro no próprio arranque
59
+ (o núcleo: o stub, o consentimento, o carregamento do `array.js`, o
60
+ `before_send`, a API) deixa a página como estava: o stub sai, o
61
+ `window.f10xTracking` fica sem `version`, e numa tag do GTM arranca a tag
62
+ oficial de reserva (*A reserva*, abaixo);
63
+ - **o cross-domain entre domínios ligados, opcional** (`linkedDomains`, desde a
64
+ 2.1.0): os links e os forms para o outro domínio levam a pessoa e a sessão do
65
+ PostHog num parâmetro que expira em 2 minutos, como o `_gl` do GA (*O
66
+ cross-domain*, abaixo).
49
67
 
50
68
  **Quem lê isto do lado do admin** (Fase A3 do plano da atribuição): a fonte
51
69
  PostHog da ingestão guarda de cada toque o `tracking_version`, a
52
70
  `page_version`, o `ab_test`, o `fingerprint`, e desde a A2b a versão do SDK e a
53
71
  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**
72
+ telefone do `contact_captured` só em hash; desde a A2c, o `tracking_fallback`
73
+ que a tag de reserva carimba e o `linked_distinct_id` do cross-domain (em hash
74
+ quando é um email, como o `distinct_id`). O `tracking_degraded` fica só no
75
+ PostHog, como o `fingerprint_ms`. **Apara cada URL pela mesma lista**
55
76
  de `src/params.js`: um parâmetro que entre na `ALLOWED_PARAMS` entra também na
56
77
  entrada `posthog` da `ingest/pii.json` e na `supabase/verify/atr3.sql` do
57
78
  admin, no mesmo PR (um teste do admin prova que as três listas são a mesma).
58
79
 
59
- ### Uma de duas instalações por site
80
+ ### Uma instalação activa por página: o snippet, e a oficial como reserva
60
81
 
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:
82
+ Cada site, de qualquer account, escolhe **uma** de duas instalações, e nunca há
83
+ duas activas na mesma página (decisão 1 da A2b, revista pela decisão 1 da A2c):
84
+ a **oficial do PostHog** (a tag no GTM, o snippet oficial no código do site, ou
85
+ o posthog-js no código de uma app) ou **este snippet** (embutido num repo
86
+ `direct`, ou pela tag do `dist/gtm.html` em qualquer outro site). Num site com
87
+ a tag do GTM, a oficial pode lá estar **só como reserva**: outra tag, que
88
+ arranca quando o snippet não carregou, e nunca ao lado dele (*A reserva*,
89
+ abaixo). Quem escolhe este não perde nada da oficial: é o mesmo SDK, com os
90
+ extras. Com duas activas, a segunda ou toma o arranque da primeira ou duplica
91
+ cada evento:
68
92
 
69
93
  - **a oficial antes deste**: o primeiro `init` da fila manda, e este não
70
94
  arranca; avisa na consola (*another PostHog is already on this page*) e o
@@ -76,8 +100,13 @@ arranque da primeira ou duplica cada evento:
76
100
  `supabase/verify/atr2b.sql` do admin guarda o resto: num host com este
77
101
  snippet, nenhum `$pageview` de outra instalação no mesmo minuto.
78
102
 
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
103
+ Num contentor GTM partilhado por vários sites, a tag oficial dispara nos hosts
104
+ que ainda não passaram ao snippet: em All Pages, com o acionador *Snippet hosts*
105
+ (o da tag do snippet) como **excepção**, e o *Direct hosts* (os sites `direct`,
106
+ com o snippet no build) como outra (Fase A2d do admin, que revê a lista de
107
+ inclusão da A2c: passar um host é uma edição da RegEx, e um host que ninguém
108
+ listou fica medido pela oficial, em vez de calado). A reserva é outra tag.
109
+ **Dentro de um
81
110
  domínio registável, todas as instalações levam a mesma data de `defaults`**: o
82
111
  cookie do PostHog é partilhado pelos subdomínios, e a data muda a regra entre o
83
112
  cookie e o `localStorage` (`cookieWinsOnConflict`). A deste pacote é
@@ -129,7 +158,7 @@ de vir no ficheiro. É uma das razões de a 2.0.0 ser major.
129
158
  O `package.json` do repo pina a versão exacta (sem `^`), e o build embute-o:
130
159
 
131
160
  ```json
132
- { "dependencies": { "@funnel10x/tracking": "2.0.0" } }
161
+ { "dependencies": { "@funnel10x/tracking": "2.2.0" } }
133
162
  ```
134
163
 
135
164
  ```js
@@ -140,6 +169,7 @@ init({
140
169
  apiHost: '/_t',
141
170
  sdkVersion: import.meta.env.VITE_POSTHOG_SDK_VERSION || undefined,
142
171
  consent: site.consent,
172
+ linkedDomains: site.linkedDomains,
143
173
  pageVersion: 3,
144
174
  test: '/pt/acq_f1/webinar/lp#2',
145
175
  })
@@ -185,34 +215,82 @@ init({
185
215
  (`f10x_consent`), e a página seguinte arranca sozinha. Com `false`, mede
186
216
  logo. **O GTM do site arranca depois do stub do snippet**, para uma tag que
187
217
  use o `window.posthog` o encontrar.
218
+ - `linkedDomains`: o do `site.json`, opcional, uma lista de hosts (*O
219
+ cross-domain*, abaixo). Sem ele, ou vazio, o cross-domain está desligado.
188
220
  - `pageVersion` e `test`: o `version` da página principal no `tests.json` (ou o
189
221
  `N` de uma variante) e o nome do teste activo, que o build entrega.
190
222
 
191
223
  Uma página cujo caminho acaba em `_` seguido de algarismos (a pré-visualização
192
224
  de uma versão) não mede nada nem mexe nos forms.
193
225
 
226
+ ## Num site com build próprio que não é da base, ou só com o header
227
+
228
+ O cartão PostHog do Account Settings dá as três maneiras, pela ordem do que é
229
+ melhor, já com a chave do projeto:
230
+
231
+ - **No build do site** (um Next.js ou um Vite na Vercel que não nasceu da
232
+ base `direct`): o `dist/direct-init.js` (o `init` com o `apiHost: '/_t'`) e
233
+ o `dist/direct-rewrites.json` (os três rewrites de cima, para o
234
+ `vercel.json`), que o build do pacote escreve e o
235
+ `tests/api/gtm-tags.test.mjs` compara com o bloco da secção de cima. O
236
+ `/_t` é o proxy no domínio do site, e por isso não leva o proxy gerido.
237
+ - **Colado no header** (um funil do GHL, um Webflow): a tag do snippet, o
238
+ `dist/gtm.html`, tal como está. Não depende do GTM, que a EasyPrivacy
239
+ bloqueia inteiro (`||googletagmanager.com^`), mas só passa pelas listas com o
240
+ proxy gerido no `apiHost`: sem ele, vai para o `i.posthog.com`, que a mesma
241
+ lista bloqueia (o `array.js` e o `/i/`). A reserva só corre pelo GTM (o
242
+ `f10x_tracking_failed` é um evento do `dataLayer`), e por isso um site sem
243
+ contentor não mede a visita em que o snippet falha.
244
+ - **Pelo GTM**: só a tag do snippet em All Pages, que é a mais simples (sem
245
+ a reserva, a visita em que o `tag.js` não carrega não se mede), ou as três
246
+ tags, abaixo.
247
+
248
+ Um host das duas primeiras num contentor com a oficial do PostHog entra no
249
+ acionador *Direct hosts*, que é a excepção dela, no dia em que passa.
250
+
194
251
  ## Num site que não é repo nosso: a tag do GTM
195
252
 
196
253
  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`.
204
-
205
- ```html
206
- <script>
207
- window.f10xTracking = {
208
- key: '{{PostHog project key}}',
209
- apiHost: '',
210
- sdkVersion: '',
211
- checkoutHosts: []
212
- }
213
- </script>
214
- <script src="https://cdn.jsdelivr.net/npm/@funnel10x/tracking@2.0.0/dist/tag.js" integrity="sha384-…" crossorigin="anonymous"></script>
215
- ```
254
+ build, o mesmo pacote carrega-se por uma tag Custom HTML do GTM, **em vez da tag
255
+ oficial do PostHog, que fica só como reserva** (*Uma instalação activa por
256
+ página*, acima, e *A reserva*, abaixo), **pela versão fixa** no URL do jsDelivr
257
+ e com o hash do ficheiro no `integrity`. **As três tags de um contentor geram-se
258
+ no admin**, já com a chave do projeto e o proxy gerido da account: em Account
259
+ Settings › Integrations, no cartão PostHog, *Install the tracking on a site*
260
+ (Fase A2d do plano da atribuição), com os acionadores e os passos de
261
+ instalar, de passar um host e de ligar o proxy. O que o cartão gera são os
262
+ ficheiros do `dist/` sem o comentário de cabeça, com os marcadores trocados, e
263
+ mais nada:
264
+
265
+ | ficheiro | a tag | dispara |
266
+ | --- | --- | --- |
267
+ | `dist/gtm.html` | o snippet, desta versão | no acionador *Snippet hosts* (Page View, *Page Hostname* corresponde à RegEx) |
268
+ | `dist/gtm-fallback.html` | a oficial do PostHog como reserva, com o `tracking_fallback` | só no evento personalizado `f10x_tracking_failed` |
269
+ | `dist/gtm-official.html` | a oficial do PostHog, para os hosts que ainda não passaram | em All Pages, com o *Snippet hosts* e o *Direct hosts* como excepção |
270
+
271
+ As três levam a condição de consentimento de analytics do site, quando ele tem
272
+ banner. Só se trocam a chave, o host (o `apiHost` no snippet, o `api_host` nas
273
+ oficiais: o proxy gerido da account, abaixo, ou o PostHog directo), os hosts do
274
+ checkout, os domínios ligados (*O cross-domain*, abaixo; vazio, desligado) e,
275
+ numa emergência, o `sdkVersion`. Uma oficial que já exista no contentor
276
+ substitui-se pela gerada, que traz a data dos `defaults` do pacote.
277
+
278
+ **Desde a 2.1.0 a tag diz quando o snippet não arrancou**: empurra para o
279
+ `dataLayer` o evento `f10x_tracking_failed`, uma vez por página, quando o
280
+ `tag.js` não chega (o `onerror`: o jsDelivr em baixo ou bloqueado, um
281
+ `integrity` que não bate, uma CSP que o recusa; ou nenhum dos dois ao fim de
282
+ 10 s, um pedido pendurado) ou chega e não deixa a `version` no
283
+ `window.f10xTracking` (o `onload`: um erro nosso no arranque). É o evento em
284
+ que a reserva dispara. Um `tag.js` que chegue depois dos 10 s encontra a
285
+ oficial e fica inerte (o `conflict`), e continua a haver uma só instalação
286
+ activa; e uma tag que dispare outra vez com o snippet já a correr não empurra
287
+ nada, mesmo que a rede lhe falhe. **O evento vai para o `window.dataLayer`**:
288
+ num GTM com a data layer renomeada (o `l=` do snippet do GTM) a reserva não
289
+ dispara, e a prova do ponto 8 do checklist apanha-o. O `<script>` do jsDelivr cria-se por código, e não como
290
+ um `<script src>` no HTML: o GTM recria cada `<script>` de um Custom HTML
291
+ copiando-lhe os atributos, e um `onload` escrito como atributo é um handler
292
+ inline, que uma CSP com nonce não deixa correr; posto como propriedade, de
293
+ dentro do script da tag, corre sempre.
216
294
 
217
295
  Uma versão nova é uma tag nova, nunca `@2` nem `@latest`: o GTM de cada site
218
296
  diz que versão dos extras corre, e o `tracking_version` de cada evento
@@ -227,7 +305,9 @@ organização, *Managed reverse proxy*) que recebe os envios e os passa ao
227
305
  PostHog, e serve também o `array.js`. Com ele, o `apiHost` da tag é esse
228
306
  subdomínio: na IR,
229
307
  `https://p.infinityresponse.com`, e na IMO Circle, `https://p.imocircle.com`
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;
308
+ (os dois válidos, lidos no PostHog a 2026-10-08; o Piko ainda não tem). O
309
+ admin guarda o de cada projeto (o *Managed proxy* do mesmo cartão, que só grava
310
+ um host que sirva o SDK do PostHog), e gera as três tags com ele. As listas que bloqueiam pelo nome do domínio deixam de o apanhar;
231
311
  o uBlock Origin no Firefox, que lê o CNAME, apanha na mesma, e quanto se
232
312
  recupera não se mede, porque o que é bloqueado não chega para se contar. O
233
313
  subdomínio é de outra origem que a página, e por isso o browser só lhe manda a
@@ -258,6 +338,55 @@ anúncio, e os campos escondidos do form lêem-nos de lá (cada campo com o
258
338
  código do form: dentro de um iframe o snippet não arranca o PostHog, só passa
259
339
  ao form o que veio no URL dele.
260
340
 
341
+ ### A reserva: a tag oficial quando o snippet não carrega
342
+
343
+ Num site com a tag do snippet, a tag oficial do PostHog passa a reserva
344
+ (decisão 1 da A2c): uma tag Custom HTML (o template da galeria não tem onde pôr
345
+ uma função) com o snippet oficial do PostHog, que dispara **só no evento
346
+ personalizado `f10x_tracking_failed`**, e por isso só quando o snippet tentou e
347
+ não conseguiu, nunca antes dele. É o `dist/gtm-fallback.html`, que o build
348
+ escreve e o Account Settings gera já preenchido: o stub do snippet oficial (a
349
+ forma que as definições do projeto dão hoje, a do `snippetFunctions` do
350
+ PostHog, que insere o `array.js` uma vez só e deixa repetir depois de uma
351
+ falha, com a lista de métodos do posthog-js da devDependency), a mesma chave e o mesmo `api_host` da tag do snippet, a data de
352
+ `defaults` e o `custom_campaign_params` do pacote (para os ids de anúncio irem
353
+ como propriedades nas duas instalações), `person_profiles: 'identified_only'`,
354
+ e um **`before_send` que carimba `tracking_fallback: true` em cada evento**, que
355
+ é o que distingue uma visita pela reserva nos dados (o admin guarda-o). Pelo
356
+ `before_send` e não pelo `register`, que persistia e marcava os `$pageview` do
357
+ snippet nas páginas seguintes.
358
+
359
+ **O trigger é um evento personalizado**, com a mesma condição de consentimento
360
+ da tag do snippet. Nunca All Pages nem *Window Loaded*: em All Pages a oficial
361
+ dispara antes de o `tag.js` chegar e toma o arranque (o `conflict`), e num site
362
+ com banner o `load` chega antes do sim, a reserva media sem ele, e quando o sim
363
+ fizesse correr a tag do snippet ela dava `conflict`. Pelo evento, a reserva já
364
+ é segura contra duplicados: com o snippet carregado o evento não sai, e se
365
+ saísse o `init` dela era um no-op no stub do snippet.
366
+
367
+ **Num contentor partilhado** (o `GTM-PM4T6FGX` do Piko) são duas tags oficiais:
368
+ a dos hosts que ainda não passaram (o `dist/gtm-official.html`), em All Pages,
369
+ sem `before_send`, com o *Snippet hosts* e o *Direct hosts* como excepção; e a
370
+ reserva, outra tag, só no `f10x_tracking_failed`, com o `before_send`. Uma tag
371
+ só a fazer as duas coisas disparava em All Pages antes do `tag.js` nos hosts do
372
+ snippet (o `conflict`) e carimbava `tracking_fallback` nos hosts da oficial, que
373
+ não é reserva nenhuma. A tag do snippet dispara só no *Snippet hosts*, e por
374
+ isso nunca nos hosts `direct` (onde o snippet vai no build) nem nos que ainda
375
+ estão na oficial.
376
+
377
+ **O que a reserva cobre**: o jsDelivr em baixo ou bloqueado, um `integrity` que
378
+ não bata, e um erro nosso no arranque. **O que não cobre**: uma versão do SDK
379
+ que parta (a oficial carrega o mesmo `array.js`, e a resposta é o
380
+ `sdkVersion`), e os bloqueadores de anúncios (bloqueiam as duas). Num site
381
+ `direct` não há reserva: o snippet vai no build, a tag do snippet não dispara
382
+ nesses hosts, e o que o protege são os extras isolados.
383
+
384
+ **O que uma visita pela reserva perde**: a visita conta, com o URL inteiro e os
385
+ parâmetros dele no PostHog, e perde-se o que é dos extras: os ids nos campos
386
+ dos forms, no URL do iframe do GHL e nos links do checkout (os `ph_*` e o
387
+ `client_reference_id`), o `contact_captured` (o email e o telefone), o
388
+ fingerprint, o `_f10x` do cross-domain, e o `tracking_version`.
389
+
261
390
  ## Os campos
262
391
 
263
392
  O que os forms, o iframe e os links do checkout levam:
@@ -297,6 +426,101 @@ O que os forms, o iframe e os links do checkout levam:
297
426
  `sale_posthog_links` liga a venda pelo `cs_…` observado no PostHog primeiro,
298
427
  senão pelo primeiro id da metadata que o projeto da account tenha visto.
299
428
 
429
+ ## O cross-domain: `linkedDomains`
430
+
431
+ **Desligado por omissão**, e só para um funil que atravesse dois domínios
432
+ registáveis nossos (decisão 3 da A2c). Entre subdomínios do mesmo domínio o
433
+ cookie do PostHog já segue a pessoa, também em sufixos de duas partes
434
+ (`.com.br`, `.co.uk`), e o que hoje atravessa domínios são terceiros (o Stripe,
435
+ o redirect dos links do GHL), que têm os seus caminhos. O posthog-js não tem
436
+ linker; este desenha-se como o `_gl` do GA.
437
+
438
+ **A opção** é uma lista de hosts (`go.marca.pt`) ou de domínios registáveis
439
+ (`marca.pt`, que apanha os subdomínios), sem esquema nem caminho, ou uma string
440
+ com eles separados por vírgulas ou espaços; uma entrada sem forma de hostname
441
+ (um `*.marca.pt`) sai, com um aviso na consola. Vai no
442
+ `window.f10xTracking` da tag, ou no `init` de um repo `direct` (o `linkedDomains`
443
+ do `site.json`, que a base passa). Os dois sites levam o outro na lista: um
444
+ `_f10x` que chegue só se lê numa página com a lista preenchida.
445
+
446
+ **À saída**, os links (`a[href]`) e os forms (`form[action]`) para um host da
447
+ lista ganham, no clique (o esquerdo, e o do meio, que abre noutro separador;
448
+ o direito não, porque o menu dele tem *Copiar endereço do link*, e uma marca
449
+ fresca no instante da cópia é o instante da partilha; e o direito tira o
450
+ `_f10x` que um clique anterior tenha deixado no link) ou no envio, o parâmetro
451
+ `_f10x=<marca de tempo, em base 36>.<i ou a>.<distinct id>.<session id>`,
452
+ codificado para o URL. Só com o PostHog a medir (com `consent: true`, depois do
453
+ sim). Ficam de fora os hosts do checkout, que têm a decoração deles, e um
454
+ destino que partilha com a página um domínio da lista (o cookie já o segue). O
455
+ `i` diz que a origem está identificada (o `distinct_id` já não é o
456
+ `$device_id`: um `contact_id` de um link de email, um identify), e o `a` que é
457
+ anónima. **O `distinct_id` vai sempre, mesmo quando é um email** (o dono,
458
+ 2026-10-14): com o sinal `i` o destino arranca identificado no PostHog, e os
459
+ eventos caem logo na pessoa do contacto. Num form GET o browser troca a query
460
+ string do `action` pelos campos, e o `_f10x` vai num campo escondido; num POST
461
+ vai no `action`.
462
+
463
+ **À chegada**, um `_f10x` cuja marca está a **menos de 2 minutos** do
464
+ carregamento da página (contados até ao carregamento, e não até ao sim do
465
+ banner; nos dois sentidos, para um relógio adiantado na origem não deixar um
466
+ link válido mais do que isso):
467
+
468
+ - **sem identidade do PostHog na página** (nada no cookie, no `localStorage` nem
469
+ no `sessionStorage` com o nome dele, `ph_<chave>_posthog`): o snippet passa
470
+ ao posthog-js o `bootstrap` com o `distinctID` e o `sessionID` da origem, e
471
+ `isIdentifiedID` quando o sinal é `i`. O primeiro `$pageview` sai com o
472
+ `distinct_id` e o `$session_id` da origem; de uma origem identificada, com um
473
+ `$device_id` novo (o `ph_anon_id` dos forms nunca é o id do contacto) e o
474
+ estado `identified`. O `bootstrap` junta-se ao que o site passe em
475
+ `posthog.bootstrap`, sem o perder;
476
+ - **com identidade** (anónima ou identificada, guardada ou dada pelo site): o
477
+ browser não a troca, porque o `bootstrap` sobrepunha-a sem evento nenhum, e
478
+ cada evento desta página leva o `linked_distinct_id` (o id que veio no link).
479
+ O admin guarda-o e liga os dois `distinct_id` como factos de ligação;
480
+ - **fora da janela**: nada. A pessoa fica com um id novo, e o admin liga-a por
481
+ outro caminho.
482
+
483
+ **Com `consent: true`** nada acontece antes do sim: o `_f10x` fica em memória,
484
+ o sim aplica-o (a janela conta desde o carregamento), e um não deita-o fora.
485
+
486
+ O que se sabe e se aceitou:
487
+
488
+ - **o `_f10x` fica no URL da página de destino**, onde qualquer tag de
489
+ terceiros o lê, e com o sinal `i` pode levar um email. O PostHog recebe o URL
490
+ inteiro, e a ingestão do admin apara-o como aos `ph_*`;
491
+ - **um link reencaminhado dentro dos 2 minutos junta duas pessoas**: é o preço
492
+ da janela, e 2 minutos é a do `_gl` (apanham 94% dos saltos medidos entre
493
+ hosts do Piko);
494
+ - **um redirect no destino que apague a query string parte o salto**, como no
495
+ GA: a aceitação de um site com a opção prova cada link;
496
+ - **um relógio adiantado no browser de origem perde a sessão, e não a
497
+ pessoa**: o posthog-js ignora um `sessionID` com mais de 1 minuto no futuro.
498
+
499
+ ## O contrato dos eventos
500
+
501
+ Um funil manda três eventos custom, e só estes (Fase A41 do plano da
502
+ atribuição, 2026-10-16):
503
+
504
+ | evento | quando | quem o manda |
505
+ | --- | --- | --- |
506
+ | `contact_captured` | o email ou o telefone sai num form, ou chega no URL (`contact_source`: `form` ou `url`), cada valor uma vez por página | o snippet |
507
+ | `checkout` | o **início** do checkout: o clique num link para um host do Stripe ou do `checkoutHosts`, uma vez por link e por página, com o `checkout_host` | o snippet, desde a 2.2.0; um checkout aberto por código (um botão com redirect, um checkout embebido) chama `f10xTracking.capture('checkout')` |
508
+ | `purchase` | o pagamento confirmado, com o `session_id` (o `cs_…` da sessão de checkout do Stripe), o `currency`, o `amount_total` e o `product` | o código do funil, na página de obrigado |
509
+
510
+ - O `contact_captured` é a ponte para o contacto, e **não uma etapa do funil**:
511
+ dispara também num form de checkout, e um opt-in, uma marcação e uma compra
512
+ dão o mesmo. O lead é o contacto no CRM.
513
+ - O `checkout` vai já, e não na fila (o `send_instantly` do posthog-js), porque
514
+ a página sai a seguir. Um site cujo código já mande o seu `checkout` tira-o
515
+ quando passar ao snippet, senão conta dois.
516
+ - O `purchase` liga a venda pelo `cs_…`, por isso tem de disparar em todas as
517
+ compras: no Piko, a 2026-10-16, o `purchase` ligava 78% das vendas e o
518
+ `checkout_completed` todas.
519
+ - Não há evento de marcação: o calendário do GHL vem num iframe, onde o
520
+ PostHog não arranca, e a marcação, a presença e o no-show vêm do GHL.
521
+ - O `lead` e o `checkout_completed` deixaram de ser contrato; o admin continua
522
+ a aceitá-los, pelo histórico e pelos funis que ainda os mandam.
523
+
300
524
  ## A API
301
525
 
302
526
  `init(options)` (ou a tag) devolve, e põe em `window.f10xTracking`:
@@ -308,8 +532,9 @@ O que os forms, o iframe e os links do checkout levam:
308
532
  - `fields()`: os valores de cima, agora;
309
533
  - `grant()`, `deny()`, `consent()`: o interruptor de um site `direct`;
310
534
  - `onReady(fn)`: quando o PostHog estiver a medir;
311
- - `capture(event, properties)`: um evento pelo mesmo PostHog (o `lead` de um
312
- form em código), que sem consentimento não faz nada;
535
+ - `capture(event, properties)`: um evento pelo mesmo PostHog (o `purchase` da
536
+ página de obrigado, o `checkout` de um checkout aberto por código), que sem
537
+ consentimento não faz nada;
313
538
  - `posthog`: a instância do PostHog depois de o SDK arrancar, e o stub até lá
314
539
  (o que se lhe chamar fica na fila, como no stub oficial). O
315
540
  `window.posthog` é o mesmo, e é por ele que as tags do GTM o usam.
@@ -397,10 +622,12 @@ Antes de ligar o tráfego:
397
622
  do `/_t` no `vercel.json`; num site de fora, a tag do `dist/gtm.html` da
398
623
  versão, com a chave da account e, se ela tiver o proxy gerido, o `apiHost`.
399
624
  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
625
+ 2. **O snippet, e a oficial como reserva**: este snippet é a única instalação
626
+ activa na página. Ao escolher este, o snippet do PostHog que o site já tenha
627
+ no head ou no código sai quando este entra, e no GTM a tag oficial passa a
628
+ reserva (*A reserva*, acima: só no `f10x_tracking_failed`, com o
629
+ `before_send`); num contentor partilhado, a oficial de sempre fica com a
630
+ condição de inclusão por hostname, e a reserva é outra tag. Com a mesma
404
631
  chave não se perde nada: quem volta mantém o id anónimo. **O `apiHost` da
405
632
  tag nova é o `api_host` que a antiga tinha** (o proxy, se ela o usava):
406
633
  vazio, perdia quem hoje chega por ele. **A data dos `defaults` é a mesma em
@@ -415,15 +642,25 @@ Antes de ligar o tráfego:
415
642
  campos de atribuição da tabela, com o *query key* igual ao nome.
416
643
  5. **Os links de email e de SMS** levam `contact_id={{contact.id}}`.
417
644
  6. **O checkout**: o host em `checkoutHosts` (o Stripe já lá está), e num
418
- checkout em código os `fields()` na metadata.
645
+ checkout em código os `fields()` na metadata e o `capture('checkout')` no
646
+ clique. Um clique de teste num link do checkout deixa um `checkout` no
647
+ PostHog, e a página de obrigado manda o `purchase` com o `cs_…` em todas as
648
+ compras (*O contrato dos eventos*, acima); nenhum outro evento custom.
419
649
  7. **Os anúncios**: o texto do gerador de links nos anúncios da Meta e da
420
650
  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`.
651
+ 8. **A prova**: na consola, com a reserva instalada,
652
+ `window.f10xTracking.mode` não é `conflict`; um pageview aberto com
653
+ `?email=teste@example.com&utm_source=teste` chega ao PostHog (Activity, no
654
+ projeto da account) uma vez só, inteiro, com o `tracking_version` da versão,
655
+ o `$lib_version` que o CDN serve e nenhum `tracking_conflict` nem
656
+ `tracking_fallback`; os campos do form e o link do checkout levam a
657
+ `utm_source` e não o `email`; e com o jsDelivr bloqueado no browser (nas
658
+ DevTools, *Network request blocking*, `cdn.jsdelivr.net`) chega um só
659
+ `$pageview`, com `tracking_fallback: true` e sem `tracking_version` (sem ele,
660
+ a reserva não disparou: o trigger, a condição de consentimento, ou um GTM com
661
+ a data layer renomeada, onde o evento não chega). Num site
662
+ com `linkedDomains`, cada link para o outro domínio abre com o `_f10x`, e o
663
+ destino, num browser limpo, com o `distinct_id` da origem.
427
664
 
428
665
  ## Desenvolver e publicar
429
666
 
@@ -442,7 +679,10 @@ serve nesse dia, e abre uma issue quando falha.
442
679
 
443
680
  Uma versão nova: subir a `version` no `package.json` (semver: uma mudança no
444
681
  que os forms ou os eventos levam é minor, uma que parte uma integração é
445
- major), `npm run build`, PR, e depois do merge, da `main`, nesta pasta:
682
+ major), `npm run build`, e no mesmo PR, em `apps-src/account-settings`, o
683
+ `npm install` e o `npm run install-build` (o Account Settings importa as três
684
+ tags do `dist/` pelo nome do pacote, e a CI falha se o bundle não for o que
685
+ elas dão); depois do merge, da `main`, nesta pasta:
446
686
 
447
687
  ```bash
448
688
  npm publish --dry-run
@@ -0,0 +1,13 @@
1
+ /*
2
+ @funnel10x/tracking@2.2.0: o arranque num site com build próprio, uma vez por carregamento da página, no browser (num Next.js, um
3
+ componente 'use client' no layout de raiz, como o Tracking.tsx da base direct). Só se muda a chave; o consent, os checkoutHosts e os
4
+ linkedDomains são do site (o README do pacote). O /_t só existe com os rewrites do dist/direct-rewrites.json no vercel.json do site.
5
+ */
6
+ import { init } from '@funnel10x/tracking'
7
+
8
+ init({
9
+ key: '{{PostHog project key}}',
10
+ apiHost: '/_t',
11
+ checkoutHosts: [],
12
+ linkedDomains: []
13
+ })
@@ -0,0 +1,7 @@
1
+ {
2
+ "rewrites": [
3
+ { "source": "/_t/static/:path(.*)", "destination": "https://us-assets.i.posthog.com/static/:path" },
4
+ { "source": "/_t/array/:path(.*)", "destination": "https://us-assets.i.posthog.com/array/:path" },
5
+ { "source": "/_t/:path(.*)", "destination": "https://us.i.posthog.com/:path" }
6
+ ]
7
+ }
@@ -0,0 +1,24 @@
1
+ <!--
2
+ @funnel10x/tracking@2.2.0: a tag de reserva do GTM (Custom HTML), num site com a tag do gtm.html (decisão 1 da A2c).
3
+ Dispara só no evento personalizado f10x_tracking_failed, com a mesma condição de consentimento da tag do snippet; nunca em All Pages
4
+ nem em Window Loaded. O before_send carimba tracking_fallback em cada evento, que é o que distingue uma visita pela reserva nos dados.
5
+ Só se mudam a chave e o api_host (o proxy gerido da account, se o tiver, e senão o PostHog directo), os mesmos da tag do gtm.html.
6
+ O Account Settings do admin (Integrations, no cartão PostHog) gera-a já preenchida, com a do snippet e os acionadores.
7
+ -->
8
+ <script>
9
+ !function(t,e){var o,n,p,r;e.__SV||(window.posthog && window.posthog.__loaded)||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}p||((p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",p.onerror=function(){p=null},(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r));var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],Object.defineProperty(u,"toString",{configurable:!0,enumerable:!0,writable:!0,value:function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e}}),Object.defineProperty(u.people,"toString",{configurable:!0,enumerable:!0,writable:!0,value:function(){return u.toString(1)+".people (stub)"}}),o="addExceptionStep alias calculateEventProperties canRenderSurvey canRenderSurveyAsync cancelPendingSurvey capture captureException captureLog captureTraceFeedback captureTraceMetric clearIdentity clear_opt_in_out_capturing createPersonProfile debug displaySurvey getActiveMatchingSurveys getAllFeatureFlags getEarlyAccessFeatures getExtension getFeatureFlag getFeatureFlagPayload getFeatureFlagResult getGroups getPageViewId getSessionProperty getSurveys get_distinct_id get_explicit_consent_status get_property get_session_id get_session_replay_url group has_opted_in_capturing has_opted_out_capturing identify init isFeatureEnabled is_capturing loadToolbar on onActiveMatchingSurveysChanged onFeatureFlags onSessionId onSurveysLoaded opt_in_capturing opt_out_capturing register register_for_session register_once reloadFeatureFlags renderSurvey reset resetGroupPropertiesForFlags resetGroups resetPersonPropertiesForFlags sessionRecordingStarted setGroupPropertiesForFlags setIdentity setInternalOrTestUser setPersonProperties setPersonPropertiesForFlags set_config shutdown startExceptionAutocapture startSessionRecording stopExceptionAutocapture stopSessionRecording unregister unregister_for_session unsetPersonProperties updateEarlyAccessFeatureEnrollment updateFlags".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
10
+ posthog.init('{{PostHog project key}}', {
11
+ api_host: 'https://us.i.posthog.com',
12
+ ui_host: 'https://us.posthog.com',
13
+ defaults: '2026-08-30',
14
+ person_profiles: 'identified_only',
15
+ custom_campaign_params: ['a_campaign_id', 'a_adset_id', 'a_ad_id', 'h_ad_id', 'fbc_id', 'utm_id', 'utm_placement', 'lp', 'narrative'],
16
+ before_send: function (event) {
17
+ if (event && event.event !== '$snapshot') {
18
+ event.properties = event.properties || {}
19
+ event.properties.tracking_fallback = true
20
+ }
21
+ return event
22
+ }
23
+ })
24
+ </script>
@@ -0,0 +1,17 @@
1
+ <!--
2
+ @funnel10x/tracking@2.2.0: a tag oficial do PostHog (Custom HTML) para os hosts que ainda não passaram ao snippet, num contentor com a tag do gtm.html.
3
+ Dispara em All Pages, com o acionador Snippet hosts (os hosts da tag do snippet) como excepção, e o Direct hosts (os sites direct, com o
4
+ snippet no build) como outra. Passar um host ao snippet é acrescentá-lo à RegEx do Snippet hosts, numa só publicação.
5
+ Só se mudam a chave e o api_host (o proxy gerido da account, se o tiver, e senão o PostHog directo), os mesmos da tag do gtm.html.
6
+ O Account Settings do admin (Integrations, no cartão PostHog) gera-a já preenchida, com a do snippet e os acionadores.
7
+ -->
8
+ <script>
9
+ !function(t,e){var o,n,p,r;e.__SV||(window.posthog && window.posthog.__loaded)||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}p||((p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",p.onerror=function(){p=null},(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r));var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],Object.defineProperty(u,"toString",{configurable:!0,enumerable:!0,writable:!0,value:function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e}}),Object.defineProperty(u.people,"toString",{configurable:!0,enumerable:!0,writable:!0,value:function(){return u.toString(1)+".people (stub)"}}),o="addExceptionStep alias calculateEventProperties canRenderSurvey canRenderSurveyAsync cancelPendingSurvey capture captureException captureLog captureTraceFeedback captureTraceMetric clearIdentity clear_opt_in_out_capturing createPersonProfile debug displaySurvey getActiveMatchingSurveys getAllFeatureFlags getEarlyAccessFeatures getExtension getFeatureFlag getFeatureFlagPayload getFeatureFlagResult getGroups getPageViewId getSessionProperty getSurveys get_distinct_id get_explicit_consent_status get_property get_session_id get_session_replay_url group has_opted_in_capturing has_opted_out_capturing identify init isFeatureEnabled is_capturing loadToolbar on onActiveMatchingSurveysChanged onFeatureFlags onSessionId onSurveysLoaded opt_in_capturing opt_out_capturing register register_for_session register_once reloadFeatureFlags renderSurvey reset resetGroupPropertiesForFlags resetGroups resetPersonPropertiesForFlags sessionRecordingStarted setGroupPropertiesForFlags setIdentity setInternalOrTestUser setPersonProperties setPersonPropertiesForFlags set_config shutdown startExceptionAutocapture startSessionRecording stopExceptionAutocapture stopSessionRecording unregister unregister_for_session unsetPersonProperties updateEarlyAccessFeatureEnrollment updateFlags".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
10
+ posthog.init('{{PostHog project key}}', {
11
+ api_host: 'https://us.i.posthog.com',
12
+ ui_host: 'https://us.posthog.com',
13
+ defaults: '2026-08-30',
14
+ person_profiles: 'identified_only',
15
+ custom_campaign_params: ['a_campaign_id', 'a_adset_id', 'a_ad_id', 'h_ad_id', 'fbc_id', 'utm_id', 'utm_placement', 'lp', 'narrative']
16
+ })
17
+ </script>
package/dist/gtm.html CHANGED
@@ -1,10 +1,45 @@
1
- <!-- @funnel10x/tracking@2.0.0: a tag do GTM (Custom HTML, em All Pages), em vez da tag oficial do PostHog e nunca ao lado dela. Só se mudam a chave, o apiHost (o proxy gerido da account, se o tiver) e, numa emergência, o sdkVersion (uma versão exacta do posthog-js); uma versão nova do pacote é um ficheiro novo. -->
1
+ <!--
2
+ @funnel10x/tracking@2.2.0: a tag do GTM (Custom HTML, em All Pages, com a condição de consentimento do site, se ele tiver banner).
3
+ Num site com esta tag, a tag oficial do PostHog só existe como reserva (decisão 1 da A2c): outra tag Custom HTML, com a mesma chave, que dispara
4
+ só no evento personalizado f10x_tracking_failed, que esta empurra para o dataLayer, uma vez, quando o tag.js não carrega (o jsDelivr em baixo ou
5
+ bloqueado, um integrity que não bate) ou carrega e não arranca (o window.f10xTracking sem version). A reserva é o dist/gtm-fallback.html, ao lado.
6
+ Só se mudam a chave, o apiHost (o proxy gerido da account, se o tiver), os checkoutHosts, os linkedDomains (o cross-domain, desligado com a
7
+ lista vazia) e, numa emergência, o sdkVersion (uma versão exacta do posthog-js); uma versão nova do pacote é um ficheiro novo.
8
+ O script do jsDelivr cria-se por código, com o onload e o onerror como propriedades: um atributo onload no HTML é um handler inline, que o GTM
9
+ copia ao recriar o script e que uma CSP com nonce não deixa correr. Um pedido pendurado conta como falhado ao fim de 10 s.
10
+ O evento vai para o window.dataLayer: num GTM com a data layer renomeada (o l= do snippet do GTM) a reserva não dispara, e o ponto 8 do
11
+ checklist do README apanha-o.
12
+ -->
2
13
  <script>
3
14
  window.f10xTracking = {
4
15
  key: '{{PostHog project key}}',
5
16
  apiHost: '',
6
17
  sdkVersion: '',
7
- checkoutHosts: []
18
+ checkoutHosts: [],
19
+ linkedDomains: []
8
20
  }
21
+ ;(function () {
22
+ var timer
23
+ function failed() {
24
+ clearTimeout(timer)
25
+ if (window.__f10xTracking && window.__f10xTracking.version) {
26
+ window.f10xTracking = window.__f10xTracking
27
+ return
28
+ }
29
+ if (window.__f10xTrackingFailed) return
30
+ window.__f10xTrackingFailed = true
31
+ ;(window.dataLayer = window.dataLayer || []).push({ event: 'f10x_tracking_failed' })
32
+ }
33
+ var script = document.createElement('script')
34
+ script.src = 'https://cdn.jsdelivr.net/npm/@funnel10x/tracking@2.2.0/dist/tag.js'
35
+ script.integrity = 'sha384-2j0oWs30fSU+6UtXb4WcrJ0Gi3Ezss5xWEwZ+jFI+8tqVvyJa3zwOPP5n+l99Ksn'
36
+ script.crossOrigin = 'anonymous'
37
+ script.onload = function () {
38
+ clearTimeout(timer)
39
+ if (!(window.f10xTracking && window.f10xTracking.version)) failed()
40
+ }
41
+ script.onerror = failed
42
+ timer = setTimeout(failed, 10000)
43
+ document.head.appendChild(script)
44
+ })()
9
45
  </script>
10
- <script src="https://cdn.jsdelivr.net/npm/@funnel10x/tracking@2.0.0/dist/tag.js" integrity="sha384-vl05BcvhQ0/QtZcEpJZuNM5QB/vPrQB7HVeN58LFs+oG43IoUfgu6Oi6SntNrjyH" crossorigin="anonymous"></script>
package/dist/index.d.ts CHANGED
@@ -17,6 +17,14 @@ export interface TrackingOptions {
17
17
  checkoutHosts?: string[]
18
18
  /** Add the missing hidden fields to lead forms (default true). */
19
19
  addFields?: boolean
20
+ /**
21
+ * Linked domains for cross-domain tracking (off by default): hosts, or registrable domains that also cover their
22
+ * subdomains, as a list or a string separated by commas or spaces (an entry that is not a hostname, like
23
+ * `*.example.com`, is ignored with a console warning). Links and forms to them carry `_f10x` on click or submit, and
24
+ * an `_f10x` that arrives within 2 minutes links the visitor: the origin's person and session when the page has no
25
+ * PostHog identity yet, or `linked_distinct_id` on every event when it has one.
26
+ */
27
+ linkedDomains?: string[] | string
20
28
  /** posthog-js options on top of this package's (turning a feature off on one site, tests). */
21
29
  posthog?: Record<string, unknown>
22
30
  }