@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 +21 -0
- package/README.md +225 -2
- package/dist/THIRD_PARTY_LICENSES.txt +873 -0
- package/dist/gtm.html +8 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +15 -0
- package/dist/tag.js +15 -0
- package/package.json +34 -4
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
|
-
#
|
|
1
|
+
# @funnel10x/tracking
|
|
2
2
|
|
|
3
|
-
|
|
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.
|