@funnel10x/tracking 0.0.0-stage → 1.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Funnel 10X
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,226 @@
1
- # Temporary Holding Version
1
+ # @funnel10x/tracking
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ O snippet de tracking dos funis da Funnel 10X: a Fase A2 do [plano da
4
+ atribuição](https://github.com/funnel10x/f10x-admin/blob/main/docs/plano-atribuicao.md)
5
+ do admin. Vive em `packages/tracking/` do `funnel10x/f10x-admin`, e publica-se
6
+ no npm como pacote público, pela versão.
7
+
8
+ O que faz numa página:
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
12
+ caminho e só os parâmetros de campanha da allow-list (`src/clean.js`), no
13
+ evento, no perfil (`$set`, `$set_once`), no cookie do PostHog, nos heatmaps
14
+ e na gravação de sessão (onde os pedidos de rede se gravam sem o corpo). O
15
+ email, o nome, o telefone e as respostas de um quiz que um funil
16
+ passe na query string ficam no browser;
17
+ - **o FingerprintJS v5** (MIT), sem o pedido de monitorização que ele faz por
18
+ omissão a um servidor deles; o `visitorId` vai em cada evento (`fingerprint`);
19
+ - **os ids e os parâmetros do anúncio nos forms**: nos campos escondidos de
20
+ cada form de lead da página (os que já existem com o nome, e os que faltam,
21
+ acrescentados), no URL do iframe de um form, inquérito ou calendário do GHL,
22
+ e nos links do checkout;
23
+ - **o identify pelo id do contacto** num link de email ou de SMS
24
+ (`?contact_id={{contact.id}}`);
25
+ - **a versão do snippet em cada evento** (`tracking_version`), e, num site
26
+ `direct`, **a versão da página e o teste** (`page_version`, `ab_test`), o
27
+ **interruptor de consentimento** do `site.json`, e a guarda das
28
+ pré-visualizações.
29
+
30
+ O posthog-js e o FingerprintJS vão dentro do ficheiro, pela versão que este
31
+ pacote fixa, e não se resolvem no site. O que fica de fora são as extensões que
32
+ o posthog-js carrega quando o projeto as liga (o gravador de sessão, os dead
33
+ clicks, os web vitals), que vêm do PostHog pela mesma versão do posthog-js
34
+ (`/static/<versão>/…`, pelo proxy num repo `direct`): esse código não passa
35
+ pelo `integrity` da tag.
36
+
37
+ ## Num repo `direct`: embutido no build
38
+
39
+ O `package.json` do repo pina a versão exacta (sem `^`), e o build embute-o:
40
+
41
+ ```json
42
+ { "dependencies": { "@funnel10x/tracking": "1.0.0" } }
43
+ ```
44
+
45
+ ```js
46
+ import { init } from '@funnel10x/tracking'
47
+
48
+ init({
49
+ key: import.meta.env.VITE_POSTHOG_KEY,
50
+ apiHost: '/_t',
51
+ consent: site.consent,
52
+ pageVersion: 3,
53
+ test: '/pt/f1/lp#2',
54
+ })
55
+ ```
56
+
57
+ - `key`: a chave do projeto PostHog da account (`phc_…`), pela variável de
58
+ ambiente da Vercel (`VITE_POSTHOG_KEY` num `static`, `NEXT_PUBLIC_POSTHOG_KEY`
59
+ num `nextjs`). É pública: vai no HTML de qualquer maneira.
60
+ - `apiHost: '/_t'`: os eventos vão pelo **proxy no domínio do site**, que livra
61
+ os envios das listas de bloqueio (a EasyPrivacy bloqueia o `posthog.com`). O
62
+ `vercel.json` do repo leva os três rewrites, por esta ordem:
63
+
64
+ ```json
65
+ {
66
+ "rewrites": [
67
+ { "source": "/_t/static/:path(.*)", "destination": "https://us-assets.i.posthog.com/static/:path" },
68
+ { "source": "/_t/array/:path(.*)", "destination": "https://us-assets.i.posthog.com/array/:path" },
69
+ { "source": "/_t/:path(.*)", "destination": "https://us.i.posthog.com/:path" }
70
+ ]
71
+ }
72
+ ```
73
+
74
+ O padrão é `:path(.*)` e não `:path*`, que não apanha a barra final dos
75
+ envios (`/e/`) e lhes responde 404 (medido em produção). O `_` à frente
76
+ garante que não colide com um funil (a base não deixa um nome
77
+ de funil começar por `_`). **E o site leva `Referrer-Policy: strict-origin`**
78
+ (nos `headers` do `vercel.json`): o proxy é do mesmo domínio, e com a política
79
+ por omissão o browser manda-lhe o URL inteiro da página no `Referer`, com o
80
+ que a query string traga, e o rewrite reencaminha-o para o PostHog.
81
+ - `consent`: o do `site.json`. Com `true`, o snippet não mede nem guarda nada
82
+ até à escolha: os forms levam só os parâmetros do anúncio tirados do URL, e o
83
+ banner do site chama `window.f10xTracking.grant()` no sim e `.deny()` no não
84
+ (ou para retirar o sim, que pára o PostHog e apaga o que ele guardou). A
85
+ escolha guarda-se em `localStorage` (`f10x_consent`), e a página seguinte
86
+ arranca sozinha. Com `false`, mede logo.
87
+ - `pageVersion` e `test`: o `version` da página principal no `tests.json` (ou o
88
+ `N` de uma variante) e o nome do teste activo, que o build entrega.
89
+
90
+ Uma página cujo caminho acaba em `_` seguido de algarismos (a pré-visualização
91
+ de uma versão) não mede nada nem mexe nos forms.
92
+
93
+ ## Num site que não é repo nosso: a tag do GTM
94
+
95
+ Num funil do GHL ou num site em Webflow, onde não há build, o mesmo pacote
96
+ carrega-se por uma tag Custom HTML do GTM (em All Pages), **pela versão fixa**
97
+ no URL do jsDelivr e com o hash do ficheiro no `integrity`. A tag de cada versão
98
+ publicada está no próprio pacote, em `dist/gtm.html`, e cola-se tal como está:
99
+ só se trocam a chave e os hosts do checkout.
100
+
101
+ ```html
102
+ <script>
103
+ window.f10xTracking = {
104
+ key: '{{PostHog project key}}',
105
+ checkoutHosts: []
106
+ }
107
+ </script>
108
+ <script src="https://cdn.jsdelivr.net/npm/@funnel10x/tracking@1.0.0/dist/tag.js" integrity="sha384-…" crossorigin="anonymous"></script>
109
+ ```
110
+
111
+ Sem `site.json` não há interruptor: o snippet corre como a tag o deixar (as
112
+ condições de consentimento da tag no GTM, se o site tiver um banner). Uma versão
113
+ nova é uma tag nova, nunca `@1` nem `@latest`: o GTM de cada site diz que versão
114
+ corre, e o `tracking_version` de cada evento confirma-o.
115
+
116
+ **O form do GHL corre num iframe** de outro domínio, onde o script da página
117
+ não chega. O snippet passa ao iframe, pelo URL dele, os ids e os parâmetros do
118
+ anúncio, e os campos escondidos do form lêem-nos de lá (cada campo com o
119
+ *query key* igual ao nome). Se o form não os ler do URL, a mesma tag entra no
120
+ código do form: dentro de um iframe o snippet não arranca o PostHog, só passa
121
+ ao form o que veio no URL dele.
122
+
123
+ ## Os campos
124
+
125
+ O que os forms, o iframe e os links do checkout levam:
126
+
127
+ | campo | o que é | sem consentimento |
128
+ | --- | --- | --- |
129
+ | `ph_anon_id` | o id do dispositivo no PostHog (o anónimo de origem) | não |
130
+ | `ph_distinct_id` | o distinct id agora (o id do contacto, depois do identify) | não |
131
+ | `ph_session_id` | a sessão do PostHog | não |
132
+ | `contact_id` | o id do contacto que veio num link desta sessão | não |
133
+ | as UTMs | `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, `utm_id`, `utm_placement` | sim, do URL |
134
+ | os click ids | `fbclid`, `gclid`, `gbraid`, `wbraid`, `gad_source`, `gad_campaignid`, `msclkid`, `ctwa_clid` | sim, do URL |
135
+ | 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 |
136
+ | `lp` | a página de entrada que o URL declara | sim, do URL |
137
+
138
+ - **Num form da página**, um campo que já existe com o nome recebe o valor se
139
+ for escondido ou estiver vazio; os que faltam acrescentam-se escondidos, só
140
+ num form de lead (com um campo de email ou de telefone) e nunca num com
141
+ `data-f10x-skip`. Voltam a preencher-se no envio, com os valores desse
142
+ momento.
143
+ - **Num form do GHL**, os campos custom do contacto com estes nomes, que a A5
144
+ lê.
145
+ - **Nos links do checkout** (os do Stripe e os hosts em `checkoutHosts`), os ids
146
+ sobrepõem-se ao que o link traga, e os parâmetros do anúncio só entram se o
147
+ link não tiver os seus. Num Payment Link do Stripe vai também o
148
+ `client_reference_id` com o distinct id, que é o que chega à sessão de
149
+ checkout.
150
+ - **Num checkout criado no servidor** (uma sessão do Stripe em código),
151
+ `window.f10xTracking.fields()` dá os mesmos valores, para a metadata.
152
+
153
+ ## A API
154
+
155
+ `init(options)` (ou a tag) devolve, e põe em `window.f10xTracking`:
156
+
157
+ - `fields()`: os valores de cima, agora;
158
+ - `grant()`, `deny()`, `consent()`: o interruptor de um site `direct`;
159
+ - `onReady(fn)`: quando o PostHog estiver a medir;
160
+ - `capture(event, properties)`: um evento pelo mesmo PostHog (o `lead` de um
161
+ form em código), que sem consentimento não faz nada;
162
+ - `posthog`: a instância, para o que não está aqui.
163
+
164
+ ## Os modelos de parâmetros de URL
165
+
166
+ **Meta**, nos *URL parameters* de cada anúncio:
167
+
168
+ ```
169
+ 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}}
170
+ ```
171
+
172
+ O nome do anúncio acaba no código do asset (decisão 10 do plano), e é por isso
173
+ que vai no `utm_content`.
174
+
175
+ **Google**, no *Final URL suffix* da conta, com o auto-tagging ligado (o
176
+ `gclid`) e dois parâmetros personalizados: `campaign` em cada campanha (o nome
177
+ dela) e `asset` em cada anúncio (o código do asset, que num anúncio de pesquisa,
178
+ num responsive display ou na Performance Max não chega pelo nome):
179
+
180
+ ```
181
+ utm_source=google&utm_medium=cpc&utm_campaign={_campaign}&utm_term={keyword}&utm_content={_asset}&campaign_id={campaignid}&adgroup_id={adgroupid}&ad_id={creative}
182
+ ```
183
+
184
+ ## O checklist de um funil
185
+
186
+ Antes de ligar o tráfego:
187
+
188
+ 1. **O snippet carrega pela versão**: num repo `direct`, a versão exacta no
189
+ `package.json`, a chave na variável de ambiente e os rewrites do `/_t` no
190
+ `vercel.json`; num site de fora, a tag do `dist/gtm.html` da versão, com a
191
+ chave da account, no GTM.
192
+ 2. **O consentimento**: o `consent` do `site.json` escrito, e o banner a chamar
193
+ `grant()` e `deny()`; num site de fora, as condições da tag no GTM.
194
+ 3. **Os forms**: cada form do GHL com os campos escondidos da tabela, com o
195
+ *query key* igual ao nome; o workflow do identify no envio; e um envio de
196
+ teste com `?utm_source=teste&gclid=teste` deixa os campos preenchidos no
197
+ contacto.
198
+ 4. **Os links de email e de SMS** levam `contact_id={{contact.id}}`.
199
+ 5. **O checkout**: o host em `checkoutHosts` (o Stripe já lá está), e num
200
+ checkout em código os `fields()` na metadata.
201
+ 6. **Os anúncios**: os parâmetros de cima na Meta e na Google.
202
+ 7. **A prova**: um pageview aberto com `?email=teste@example.com&utm_source=teste`
203
+ chega ao PostHog (Activity, no projeto da account) com a `utm_source` e sem
204
+ o `email`, no evento e no perfil, e com o `tracking_version` da versão.
205
+
206
+ ## Desenvolver e publicar
207
+
208
+ ```bash
209
+ npm ci
210
+ npm run build
211
+ ```
212
+
213
+ O `dist/` commita-se, como o build das apps do admin, e a CI refaz-o e falha se
214
+ não der os mesmos bytes: o que se publica é o que se testou. Os testes estão no
215
+ admin, em `tests/api/tracking.test.mjs` (a limpeza, sem browser) e
216
+ `tests/tracking.spec.mjs` (o snippet numa página, contra o `dist/`).
217
+
218
+ Uma versão nova: subir a `version` no `package.json` (semver: uma mudança no
219
+ que os forms ou os eventos levam é minor, uma que parte uma integração é
220
+ major), `npm run build`, PR, e depois do merge, da `main`:
221
+
222
+ ```bash
223
+ npm publish
224
+ ```
225
+
226
+ O `dist/THIRD_PARTY_LICENSES.txt` leva as licenças do que vai embutido.