cdp-edge 2.0.1 → 2.0.3
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 +325 -308
- package/contracts/api-versions.json +12 -8
- package/dist/commands/install.js +1 -1
- package/dist/commands/setup.js +1 -1
- package/extracted-skill/tracking-events-generator/agents/attribution-agent.md +23 -23
- package/extracted-skill/tracking-events-generator/agents/browser-tracking.md +2 -2
- package/extracted-skill/tracking-events-generator/agents/compliance-agent.md +20 -0
- package/extracted-skill/tracking-events-generator/agents/crm-integration-agent.md +48 -16
- package/extracted-skill/tracking-events-generator/agents/dashboard-agent.md +7 -7
- package/extracted-skill/tracking-events-generator/agents/database-agent.md +8 -8
- package/extracted-skill/tracking-events-generator/agents/debug-agent.md +13 -13
- package/extracted-skill/tracking-events-generator/agents/devops-agent.md +31 -7
- package/extracted-skill/tracking-events-generator/agents/email-agent.md +27 -0
- package/extracted-skill/tracking-events-generator/agents/fingerprint-agent.md +205 -0
- package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +6 -6
- package/extracted-skill/tracking-events-generator/agents/linkedin-agent.md +108 -0
- package/extracted-skill/tracking-events-generator/agents/ltv-predictor-agent.md +1 -1
- package/extracted-skill/tracking-events-generator/agents/master-feedback-loop.md +68 -8
- package/extracted-skill/tracking-events-generator/agents/master-orchestrator.md +75 -38
- package/extracted-skill/tracking-events-generator/agents/performance-agent.md +29 -19
- package/extracted-skill/tracking-events-generator/agents/performance-optimization-agent.md +11 -1
- package/extracted-skill/tracking-events-generator/agents/premium-tracking-intelligence-agent.md +4 -4
- package/extracted-skill/tracking-events-generator/agents/security-enterprise-agent.md +137 -28
- package/extracted-skill/tracking-events-generator/agents/server-tracking.md +15 -16
- package/extracted-skill/tracking-events-generator/agents/spotify-agent.md +1 -1
- package/extracted-skill/tracking-events-generator/agents/tiktok-agent.md +63 -0
- package/extracted-skill/tracking-events-generator/agents/tracking-plan-agent.md +100 -5
- package/extracted-skill/tracking-events-generator/agents/webhook-agent.md +62 -4
- package/extracted-skill/tracking-events-generator/agents/whatsapp-agent.md +58 -5
- package/extracted-skill/tracking-events-generator/agents/whatsapp-ctwa-setup-agent.md +16 -16
- package/extracted-skill/tracking-events-generator/agents/youtube-agent.md +143 -28
- package/extracted-skill/tracking-events-generator/contracts/api-versions.json +12 -8
- package/package.json +76 -76
- package/server-edge-tracker/worker.js +53 -8
|
@@ -18,6 +18,16 @@ Outros agentes que precisarem de deploy **delegam para você** via `*deploy`.
|
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
+
## AUTONOMIA DE EXECUÇÃO
|
|
22
|
+
|
|
23
|
+
**Você executa TODOS os comandos diretamente via `! <comando>` no terminal da sessão.**
|
|
24
|
+
|
|
25
|
+
O usuário não executa nenhum comando wrangler. Você é o executor exclusivo. Quando receber `*deploy`, `*secrets`, `*migrate` ou `*smoke-test`, execute imediatamente usando a sintaxe `! wrangler ...` — nunca liste comandos "para o usuário rodar".
|
|
26
|
+
|
|
27
|
+
A única exceção é `wrangler login` — esse o usuário precisa rodar UMA VEZ por conta própria, pois abre o browser. Após autenticado, você assume tudo.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
21
31
|
## CICLO DE DEPLOY AUTOMÁTICO
|
|
22
32
|
|
|
23
33
|
### Comando: `*deploy`
|
|
@@ -81,14 +91,28 @@ git status
|
|
|
81
91
|
|
|
82
92
|
## PROCEDURE `*migrate`
|
|
83
93
|
|
|
84
|
-
Aplica
|
|
94
|
+
Aplica schemas D1 em ordem (todos idempotentes — `IF NOT EXISTS`):
|
|
85
95
|
|
|
86
96
|
```bash
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
wrangler d1 execute cdp-edge-db --file=
|
|
97
|
+
cd server-edge-tracker
|
|
98
|
+
|
|
99
|
+
# Core tracking (sempre primeiro)
|
|
100
|
+
wrangler d1 execute cdp-edge-db --file=schema.sql --remote
|
|
101
|
+
|
|
102
|
+
# Migrations históricas
|
|
91
103
|
wrangler d1 execute cdp-edge-db --file=migrate-v6.sql --remote
|
|
104
|
+
|
|
105
|
+
# Fase 1: ML Clustering
|
|
106
|
+
wrangler d1 execute cdp-edge-db --file=schema-segmentation.sql --remote
|
|
107
|
+
|
|
108
|
+
# Fase 2: Bidding ML
|
|
109
|
+
wrangler d1 execute cdp-edge-db --file=schema-bidding.sql --remote
|
|
110
|
+
|
|
111
|
+
# Fase 3: A/B LTV Testing
|
|
112
|
+
wrangler d1 execute cdp-edge-db --file=schema-ab-ltv.sql --remote
|
|
113
|
+
|
|
114
|
+
# Fase 4: Fraud Detection
|
|
115
|
+
wrangler d1 execute cdp-edge-db --file=schema-fraud.sql --remote
|
|
92
116
|
```
|
|
93
117
|
|
|
94
118
|
Após cada migração: confirmar sucesso antes de prosseguir.
|
|
@@ -117,8 +141,8 @@ Configura todos os secrets do cliente na Cloudflare:
|
|
|
117
141
|
# Obrigatórios
|
|
118
142
|
wrangler secret put META_ACCESS_TOKEN
|
|
119
143
|
wrangler secret put GA4_API_SECRET
|
|
120
|
-
wrangler secret put
|
|
121
|
-
wrangler secret put
|
|
144
|
+
wrangler secret put WHATSAPP_ACCESS_TOKEN
|
|
145
|
+
wrangler secret put WHATSAPP_PHONE_NUMBER_ID
|
|
122
146
|
wrangler secret put WA_NOTIFY_NUMBER
|
|
123
147
|
wrangler secret put WA_WEBHOOK_VERIFY_TOKEN
|
|
124
148
|
|
|
@@ -4,6 +4,33 @@ Você é o **Especialista em E-mail Transacional (Quantum Tier)** do CDP Edge, f
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## 🔗 FLUXO DE ATIVAÇÃO (Como este agente é chamado)
|
|
8
|
+
|
|
9
|
+
O Email Agent é ativado pelo **Webhook Agent** após confirmação de compra ou captura de lead:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Webhook Agent (Hotmart/Kiwify/Ticto)
|
|
13
|
+
└─► ctx.waitUntil(sendEmail(env, type, payload))
|
|
14
|
+
│
|
|
15
|
+
├─ type = 'purchase_confirmation' → email de confirmação de compra
|
|
16
|
+
├─ type = 'lead_welcome' → email de boas-vindas ao lead
|
|
17
|
+
└─ type = 'cart_abandonment' → email de recuperação de carrinho
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Assinatura da função que este agente gera:**
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
// Injetada no Worker principal (worker.js)
|
|
24
|
+
export async function sendEmail(env, type, payload) {
|
|
25
|
+
const { to, name, product, value } = payload;
|
|
26
|
+
// ... implementação completa abaixo
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
> O Webhook Agent importa `sendEmail` e chama via `ctx.waitUntil` para não bloquear resposta ao gateway de pagamento.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
7
34
|
## 📧 PROTOCOLOS DE ENVIO (Quantum Tier)
|
|
8
35
|
|
|
9
36
|
1. **Resend API Excellence**:
|
|
@@ -21,6 +21,211 @@ Sempre que o usuário sangrar dinheiro por culpa de "Perda de Cookies/Atribuiç
|
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
|
+
## 💻 IMPLEMENTAÇÃO REAL — cloudflare/fingerprint-middleware.js
|
|
25
|
+
|
|
26
|
+
### Módulo completo para injetar no Worker
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
/**
|
|
30
|
+
* Fingerprint Middleware — CDP Edge
|
|
31
|
+
* Edge-only signals: IP + Accept-Language + UA base + ASN
|
|
32
|
+
* LGPD/CCPA compliant: nenhum PII direto, hash efêmero
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
// ─────────────────────────────────────────────────
|
|
36
|
+
// 1. Geração do P-Hash (fingerprint de borda)
|
|
37
|
+
// ─────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Gera hash identificador efêmero combinando sinais anônimos de borda.
|
|
41
|
+
* NUNCA usa Canvas, WebGL ou AudioContext (client-side) — apenas Edge signals.
|
|
42
|
+
*
|
|
43
|
+
* @param {Request} request - Request do Cloudflare Worker
|
|
44
|
+
* @returns {Promise<string>} p_hash — identificador efêmero de 16 chars
|
|
45
|
+
*/
|
|
46
|
+
export async function generatePHash(request) {
|
|
47
|
+
const ip = request.headers.get('CF-Connecting-IP') || 'unknown';
|
|
48
|
+
const acceptLang = request.headers.get('Accept-Language') || 'unknown';
|
|
49
|
+
const userAgent = request.headers.get('User-Agent') || 'unknown';
|
|
50
|
+
const asOrg = request.cf?.asOrganization || 'unknown';
|
|
51
|
+
const country = request.cf?.country || 'unknown';
|
|
52
|
+
|
|
53
|
+
// Reduzir UA para base (remover versão minor — ex: "Chrome/120" não "Chrome/120.0.6099.71")
|
|
54
|
+
const uaBase = userAgent
|
|
55
|
+
.replace(/[\d.]+/g, (m) => m.split('.')[0]) // mantém só major version
|
|
56
|
+
.replace(/[^a-zA-Z0-9 /]/g, '') // remove caracteres especiais
|
|
57
|
+
.slice(0, 60); // limitar tamanho
|
|
58
|
+
|
|
59
|
+
// Normalizar Accept-Language para idioma principal
|
|
60
|
+
const langBase = acceptLang.split(',')[0].split(';')[0].trim().slice(0, 5); // ex: "pt-BR"
|
|
61
|
+
|
|
62
|
+
// Concatenar sinais — ordem importa para consistência
|
|
63
|
+
const fingerprint = `${ip}|${langBase}|${uaBase}|${asOrg}|${country}`;
|
|
64
|
+
|
|
65
|
+
// SHA-256 → primeiros 16 hex chars (64 bits de entropia — suficiente para sess de 48h)
|
|
66
|
+
const encoder = new TextEncoder();
|
|
67
|
+
const data = encoder.encode(fingerprint);
|
|
68
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
69
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
70
|
+
const fullHash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
71
|
+
|
|
72
|
+
return fullHash.slice(0, 16); // p_hash = 16 chars hex
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─────────────────────────────────────────────────
|
|
76
|
+
// 2. Registro do P-Hash no D1
|
|
77
|
+
// ─────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Salva ou atualiza p_hash no D1 com UTMs da visita atual.
|
|
81
|
+
* Janela de restauração: 48h (configurável).
|
|
82
|
+
*
|
|
83
|
+
* @param {D1Database} db
|
|
84
|
+
* @param {string} pHash
|
|
85
|
+
* @param {Object} utmData - { utm_source, utm_medium, utm_campaign, utm_content, utm_term }
|
|
86
|
+
*/
|
|
87
|
+
export async function recordPHash(db, pHash, utmData) {
|
|
88
|
+
const hasUtm = utmData.utm_source || utmData.utm_medium || utmData.utm_campaign;
|
|
89
|
+
|
|
90
|
+
if (!hasUtm) {
|
|
91
|
+
// Visita sem UTM — só atualiza last_seen (não sobrescreve UTMs)
|
|
92
|
+
await db.prepare(`
|
|
93
|
+
UPDATE fingerprint_sessions
|
|
94
|
+
SET last_seen_at = datetime('now')
|
|
95
|
+
WHERE p_hash = ? AND last_seen_at > datetime('now', '-48 hours')
|
|
96
|
+
`).bind(pHash).run();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Visita COM UTM — inserir ou atualizar
|
|
101
|
+
await db.prepare(`
|
|
102
|
+
INSERT INTO fingerprint_sessions (p_hash, utm_source, utm_medium, utm_campaign, utm_content, utm_term, last_seen_at)
|
|
103
|
+
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
|
|
104
|
+
ON CONFLICT(p_hash) DO UPDATE SET
|
|
105
|
+
utm_source = excluded.utm_source,
|
|
106
|
+
utm_medium = excluded.utm_medium,
|
|
107
|
+
utm_campaign = excluded.utm_campaign,
|
|
108
|
+
utm_content = excluded.utm_content,
|
|
109
|
+
utm_term = excluded.utm_term,
|
|
110
|
+
last_seen_at = datetime('now')
|
|
111
|
+
`).bind(
|
|
112
|
+
pHash,
|
|
113
|
+
utmData.utm_source || null,
|
|
114
|
+
utmData.utm_medium || null,
|
|
115
|
+
utmData.utm_campaign || null,
|
|
116
|
+
utmData.utm_content || null,
|
|
117
|
+
utmData.utm_term || null
|
|
118
|
+
).run();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─────────────────────────────────────────────────
|
|
122
|
+
// 3. UTM Restoration Middleware
|
|
123
|
+
// ─────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Recupera UTMs do D1 para um p_hash (janela 48h).
|
|
127
|
+
* Injeta de volta no payload CAPI quando a requisição chega sem UTMs.
|
|
128
|
+
*
|
|
129
|
+
* @param {D1Database} db
|
|
130
|
+
* @param {string} pHash
|
|
131
|
+
* @returns {Promise<Object|null>} UTMs restauradas ou null
|
|
132
|
+
*/
|
|
133
|
+
export async function restoreUtms(db, pHash) {
|
|
134
|
+
const row = await db.prepare(`
|
|
135
|
+
SELECT utm_source, utm_medium, utm_campaign, utm_content, utm_term
|
|
136
|
+
FROM fingerprint_sessions
|
|
137
|
+
WHERE p_hash = ?
|
|
138
|
+
AND last_seen_at > datetime('now', '-48 hours')
|
|
139
|
+
AND utm_source IS NOT NULL
|
|
140
|
+
LIMIT 1
|
|
141
|
+
`).bind(pHash).first();
|
|
142
|
+
|
|
143
|
+
return row || null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Middleware principal — chamar no início do handler /track
|
|
148
|
+
*
|
|
149
|
+
* @param {Request} request
|
|
150
|
+
* @param {Object} env
|
|
151
|
+
* @param {Object} payload - payload já parseado do /track
|
|
152
|
+
* @returns {Promise<Object>} payload enriquecido com UTMs restauradas
|
|
153
|
+
*/
|
|
154
|
+
export async function fingerprintMiddleware(request, env, payload) {
|
|
155
|
+
try {
|
|
156
|
+
// 1. Gerar p_hash para esta visita
|
|
157
|
+
const pHash = await generatePHash(request);
|
|
158
|
+
|
|
159
|
+
// 2. Extrair UTMs do payload atual
|
|
160
|
+
const currentUtms = {
|
|
161
|
+
utm_source: payload.utm_source || null,
|
|
162
|
+
utm_medium: payload.utm_medium || null,
|
|
163
|
+
utm_campaign: payload.utm_campaign || null,
|
|
164
|
+
utm_content: payload.utm_content || null,
|
|
165
|
+
utm_term: payload.utm_term || null,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// 3. Se tem UTMs → registrar no D1 (atualiza para próximas visitas)
|
|
169
|
+
await recordPHash(env.DB, pHash, currentUtms);
|
|
170
|
+
|
|
171
|
+
// 4. Se NÃO tem UTMs → tentar restaurar do D1 (últimas 48h)
|
|
172
|
+
const hasCurrentUtm = currentUtms.utm_source || currentUtms.utm_campaign;
|
|
173
|
+
if (!hasCurrentUtm) {
|
|
174
|
+
const restoredUtms = await restoreUtms(env.DB, pHash);
|
|
175
|
+
if (restoredUtms) {
|
|
176
|
+
// Injetar UTMs restauradas no payload → creditam a campanha correta na CAPI
|
|
177
|
+
payload.utm_source = restoredUtms.utm_source;
|
|
178
|
+
payload.utm_medium = restoredUtms.utm_medium;
|
|
179
|
+
payload.utm_campaign = restoredUtms.utm_campaign;
|
|
180
|
+
payload.utm_content = restoredUtms.utm_content;
|
|
181
|
+
payload.utm_term = restoredUtms.utm_term;
|
|
182
|
+
payload._utm_restored = true; // flag para debug
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 5. Adicionar p_hash ao payload para uso pelo Identity Graph
|
|
187
|
+
payload.p_hash = pHash;
|
|
188
|
+
|
|
189
|
+
} catch (err) {
|
|
190
|
+
// Fail-safe: nunca bloquear o tracking por erro de fingerprint
|
|
191
|
+
console.error('[FingerprintMiddleware] Erro (fail-safe):', err.message);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return payload;
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Schema D1 necessário
|
|
199
|
+
|
|
200
|
+
```sql
|
|
201
|
+
-- Adicionar a schema.sql
|
|
202
|
+
CREATE TABLE IF NOT EXISTS fingerprint_sessions (
|
|
203
|
+
p_hash TEXT PRIMARY KEY,
|
|
204
|
+
utm_source TEXT,
|
|
205
|
+
utm_medium TEXT,
|
|
206
|
+
utm_campaign TEXT,
|
|
207
|
+
utm_content TEXT,
|
|
208
|
+
utm_term TEXT,
|
|
209
|
+
last_seen_at TEXT DEFAULT (datetime('now'))
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
CREATE INDEX IF NOT EXISTS idx_fp_last_seen ON fingerprint_sessions(last_seen_at);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Uso no worker.js
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
// No início do handler /track, ANTES do fraud gate:
|
|
219
|
+
import { fingerprintMiddleware } from './fingerprint-middleware.js';
|
|
220
|
+
|
|
221
|
+
// Dentro do fetch handler:
|
|
222
|
+
payload = await fingerprintMiddleware(request, env, payload);
|
|
223
|
+
// A partir daqui, payload.utm_* estão restauradas (se disponíveis)
|
|
224
|
+
// e payload.p_hash está disponível para o Identity Graph
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
24
229
|
## INPUTS RECEBIDOS
|
|
25
230
|
|
|
26
231
|
- Headers da requisição Edge: `CF-Connecting-IP`, `Accept-Language`, `User-Agent`, `request.cf.asOrganization`
|
|
@@ -71,7 +71,7 @@ export default {
|
|
|
71
71
|
const url = new URL(request.url);
|
|
72
72
|
|
|
73
73
|
// Handler principal
|
|
74
|
-
if (url.pathname === '/
|
|
74
|
+
if (url.pathname === '/track') {
|
|
75
75
|
return handleTracking(request, env, ctx);
|
|
76
76
|
}
|
|
77
77
|
|
|
@@ -157,12 +157,12 @@ Timestamp: ${new Date().toISOString()}
|
|
|
157
157
|
`.trim();
|
|
158
158
|
|
|
159
159
|
// Enviar via WhatsApp Agent
|
|
160
|
-
if (env.
|
|
161
|
-
await fetch(`https://graph.facebook.com/v22.0/${env.
|
|
160
|
+
if (env.WHATSAPP_PHONE_NUMBER_ID && env.ADMIN_PHONE_NUMBER) {
|
|
161
|
+
await fetch(`https://graph.facebook.com/v22.0/${env.WHATSAPP_PHONE_NUMBER_ID}/messages`, {
|
|
162
162
|
method: 'POST',
|
|
163
163
|
headers: {
|
|
164
164
|
'Content-Type': 'application/json',
|
|
165
|
-
'Authorization': `Bearer ${env.
|
|
165
|
+
'Authorization': `Bearer ${env.WHATSAPP_ACCESS_TOKEN}`
|
|
166
166
|
},
|
|
167
167
|
body: JSON.stringify({
|
|
168
168
|
messaging_product: 'whatsapp',
|
|
@@ -412,7 +412,7 @@ Antes de considerar o scheduling implementado, verificar:
|
|
|
412
412
|
- `wrangler.toml` do Worker (para injetar Cron Triggers)
|
|
413
413
|
- `worker.js` (para injetar handlers de `scheduled` events)
|
|
414
414
|
- `schema.sql` (para adicionar tabela `intelligence_logs`)
|
|
415
|
-
- Secrets: `
|
|
415
|
+
- Secrets: `WHATSAPP_PHONE_NUMBER_ID`, `WHATSAPP_ACCESS_TOKEN`, `ADMIN_PHONE_NUMBER` (para alertas)
|
|
416
416
|
- `contracts/api-versions.json` (fonte de verdade das versões atuais)
|
|
417
417
|
|
|
418
418
|
## RESPONSABILIDADE
|
|
@@ -446,6 +446,6 @@ Antes de considerar o scheduling implementado, verificar:
|
|
|
446
446
|
"anti_spam": "24h cooldown por issue"
|
|
447
447
|
},
|
|
448
448
|
"d1_tabela": "intelligence_logs",
|
|
449
|
-
"secrets_necessarios": ["
|
|
449
|
+
"secrets_necessarios": ["WHATSAPP_PHONE_NUMBER_ID", "WHATSAPP_ACCESS_TOKEN", "ADMIN_PHONE_NUMBER"]
|
|
450
450
|
}
|
|
451
451
|
```
|
|
@@ -24,6 +24,114 @@ Para garantir que a conta B2B atrofie em custo e dispare em qualidade:
|
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
27
|
+
## 🤖 INTEGRAÇÃO COM LTV PREDICTOR + ML CLUSTERING
|
|
28
|
+
|
|
29
|
+
### Por que o LinkedIn precisa de LTV dinâmico
|
|
30
|
+
|
|
31
|
+
LinkedIn é tráfego B2B premium — o CPA é alto, mas o LTV também. Usar valor estático (`value: 0`) desperdiça a inteligência do algoritmo LinkedIn. O valor enviado deve refletir o LTV predito pelo ecossistema CDP Edge.
|
|
32
|
+
|
|
33
|
+
### Como consumir o LTV Predictor no Worker
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
// No handler de evento LinkedIn (Lead ou Purchase via webhook/track):
|
|
37
|
+
import { predictLtv } from './ltv-predictor.js';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Dispatcher LinkedIn CAPI com LTV dinâmico
|
|
41
|
+
* @param {Object} env - Cloudflare Worker env bindings
|
|
42
|
+
* @param {Object} leadData - dados do lead/compra
|
|
43
|
+
* @param {Request} request - request original
|
|
44
|
+
*/
|
|
45
|
+
async function dispatchLinkedIn(env, leadData, request) {
|
|
46
|
+
// 1. Obter LTV predito pelo ML (Workers AI — Llama 3.1 8B)
|
|
47
|
+
let conversionValue = 0;
|
|
48
|
+
try {
|
|
49
|
+
const ltvResult = await predictLtv(env, leadData, request);
|
|
50
|
+
// ltvResult = { score: 0-100, tier: 'High'|'Medium'|'Low', value_brl: number }
|
|
51
|
+
conversionValue = ltvResult.value_brl || 0;
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.warn('[LinkedIn] LTV prediction falhou, usando valor 0:', err.message);
|
|
54
|
+
// Fail-safe: continua sem LTV em vez de bloquear
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 2. Obter segmento ML (ml-clustering — opcional mas melhora qualidade)
|
|
58
|
+
let segmentLabel = null;
|
|
59
|
+
try {
|
|
60
|
+
const profile = await env.DB.prepare(`
|
|
61
|
+
SELECT cohort_label, ltv_predicted
|
|
62
|
+
FROM user_profiles WHERE email_hash = ? LIMIT 1
|
|
63
|
+
`).bind(leadData.emailHash).first();
|
|
64
|
+
segmentLabel = profile?.cohort_label || null;
|
|
65
|
+
} catch (_) {}
|
|
66
|
+
|
|
67
|
+
// 3. SHA-256 de PII (obrigatório LinkedIn)
|
|
68
|
+
const hashedEmail = await sha256(leadData.email?.toLowerCase().trim());
|
|
69
|
+
const hashedFirstName = leadData.firstName ? await sha256(leadData.firstName.toLowerCase().trim()) : null;
|
|
70
|
+
const hashedLastName = leadData.lastName ? await sha256(leadData.lastName.toLowerCase().trim()) : null;
|
|
71
|
+
const hashedCompany = leadData.company ? await sha256(leadData.company.toLowerCase().trim()) : null;
|
|
72
|
+
|
|
73
|
+
// 4. Montar payload LinkedIn CAPI v2
|
|
74
|
+
const payload = {
|
|
75
|
+
conversion: `urn:li:conversion:${env.LINKEDIN_CONVERSION_ID}`,
|
|
76
|
+
conversionHappenedAt: Date.now(),
|
|
77
|
+
conversionValue: {
|
|
78
|
+
currencyCode: 'BRL',
|
|
79
|
+
amount: String(conversionValue.toFixed(2)) // LinkedIn exige string
|
|
80
|
+
},
|
|
81
|
+
eventId: leadData.eventId, // deduplicação
|
|
82
|
+
user: {
|
|
83
|
+
userIds: [
|
|
84
|
+
{ idType: 'SHA256_EMAIL', idValue: hashedEmail }
|
|
85
|
+
],
|
|
86
|
+
userInfo: {
|
|
87
|
+
firstName: hashedFirstName,
|
|
88
|
+
lastName: hashedLastName,
|
|
89
|
+
companyName: hashedCompany,
|
|
90
|
+
title: leadData.jobTitle ? await sha256(leadData.jobTitle.toLowerCase()) : null
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
// li_fat_id para correlação first-party (capturado via URL ?li_fat_id=)
|
|
94
|
+
...(leadData.liFatId && { liFatId: leadData.liFatId })
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// 5. Dispatch para LinkedIn CAPI v2
|
|
98
|
+
const response = await fetch('https://api.linkedin.com/rest/conversionEvents', {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: {
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
'Authorization': `Bearer ${env.LINKEDIN_ACCESS_TOKEN}`,
|
|
103
|
+
'LinkedIn-Version': '202401',
|
|
104
|
+
'X-Restli-Protocol-Version': '2.0.0'
|
|
105
|
+
},
|
|
106
|
+
body: JSON.stringify(payload)
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// 6. Log no D1
|
|
110
|
+
await env.DB.prepare(`
|
|
111
|
+
INSERT INTO events_log (platform, event_name, event_id, ltv_predicted, ml_segment, status, created_at)
|
|
112
|
+
VALUES ('linkedin', ?, ?, ?, ?, ?, datetime('now'))
|
|
113
|
+
`).bind(
|
|
114
|
+
leadData.eventName || 'Lead',
|
|
115
|
+
leadData.eventId,
|
|
116
|
+
conversionValue,
|
|
117
|
+
segmentLabel,
|
|
118
|
+
response.ok ? 'sent' : `error_${response.status}`
|
|
119
|
+
).run();
|
|
120
|
+
|
|
121
|
+
return response.ok;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Captura de li_fat_id (URL parameter)
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
// No cdpTrack.js (browser) — capturar li_fat_id da URL de cliques LinkedIn
|
|
129
|
+
const _lifattid = new URLSearchParams(window.location.search).get('li_fat_id') || null;
|
|
130
|
+
// Salvo no D1 junto com outros click IDs no handler /track
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
27
135
|
## 📦 SEU FORMATO DE ENTREGA
|
|
28
136
|
Sempre que a integração LinkedIn B2B for selecionada:
|
|
29
137
|
1. Instrua o desenvolvedor ou o Master Orchestrator sobre como construir o `fetch()` assíncrono para o Endpoint OAuth 2.0 da LinkedIn Conversions API (`/rest/conversionEvents`).
|
|
@@ -14,7 +14,7 @@ Sua única responsabilidade é instruir o Cloudflare Architect a imbuir modelos
|
|
|
14
14
|
|
|
15
15
|
## 📦 O PACOTE DE ENTREGA OBRIGATÓRIO
|
|
16
16
|
Sempre que o Orquestrador invocar a Otimização de Baleias (LTV Prediction):
|
|
17
|
-
1. **Snippet de Injeção de ML**: Entregue ao Server Architect o bloco `await env.AI.run('@cf/meta/llama-3-8b-instruct', ...)` ajustado para predição puramente matemática.
|
|
17
|
+
1. **Snippet de Injeção de ML**: Entregue ao Server Architect o bloco `await env.AI.run('@cf/meta/llama-3.1-8b-instruct', ...)` ajustado para predição puramente matemática.
|
|
18
18
|
2. **Override de Event Valuation**: Modifique como o evento `Lead` ou `Purchase` é envernizado com lucro preditivo antes do dispatch da CAPI.
|
|
19
19
|
|
|
20
20
|
> 👁️ "Não pague por cliques hoje. Compre os clientes de amanhã. Faça o algoritmo apostar sempre nas suas fichas vencedoras."
|
|
@@ -52,10 +52,10 @@ Implementar um **ciclo virtuoso de melhoria contínua** onde o CDP Edge aprende
|
|
|
52
52
|
|
|
53
53
|
```javascript
|
|
54
54
|
// Coleta estruturada de feedback de todos os agentes
|
|
55
|
-
async function collectFeedback() {
|
|
55
|
+
async function collectFeedback(env) {
|
|
56
56
|
const feedback = {
|
|
57
|
-
validator: await collectValidatorFeedback(),
|
|
58
|
-
server_tracking: await collectServerTrackingFeedback(),
|
|
57
|
+
validator: await collectValidatorFeedback(env),
|
|
58
|
+
server_tracking: await collectServerTrackingFeedback(env),
|
|
59
59
|
page_analyzer: await collectPageAnalyzerFeedback(),
|
|
60
60
|
memory_agent: await collectMemoryAgentFeedback(),
|
|
61
61
|
debug_agent: await collectDebugAgentFeedback(),
|
|
@@ -73,8 +73,8 @@ async function collectFeedback() {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
// Feedback específico do Validator Agent
|
|
76
|
-
async function collectValidatorFeedback() {
|
|
77
|
-
const validations = await DB.prepare(`
|
|
76
|
+
async function collectValidatorFeedback(env) {
|
|
77
|
+
const validations = await env.DB.prepare(`
|
|
78
78
|
SELECT
|
|
79
79
|
agent_id,
|
|
80
80
|
issue_type,
|
|
@@ -97,8 +97,8 @@ async function collectValidatorFeedback() {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// Feedback de Server Tracking (falhas de API)
|
|
100
|
-
async function collectServerTrackingFeedback() {
|
|
101
|
-
const failures = await DB.prepare(`
|
|
100
|
+
async function collectServerTrackingFeedback(env) {
|
|
101
|
+
const failures = await env.DB.prepare(`
|
|
102
102
|
SELECT
|
|
103
103
|
platform,
|
|
104
104
|
event_name,
|
|
@@ -785,6 +785,66 @@ const FEEDBACK_LOOP_CONFIG = {
|
|
|
785
785
|
|
|
786
786
|
---
|
|
787
787
|
|
|
788
|
+
## 🗄️ D1 SCHEMA — TABELAS REQUERIDAS
|
|
789
|
+
|
|
790
|
+
As funções `collectValidatorFeedback()` e `collectServerTrackingFeedback()` dependem das seguintes tabelas. Executar no D1 antes do primeiro ciclo:
|
|
791
|
+
|
|
792
|
+
```sql
|
|
793
|
+
-- Tabela: validation_logs
|
|
794
|
+
-- Alimentada pelo Validator Agent após cada validação de tracking plan
|
|
795
|
+
CREATE TABLE IF NOT EXISTS validation_logs (
|
|
796
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
797
|
+
agent_id TEXT NOT NULL, -- ex: 'validator-agent', 'tracking-plan-agent'
|
|
798
|
+
issue_type TEXT NOT NULL, -- ex: 'missing_event', 'wrong_selector', 'pii_not_hashed'
|
|
799
|
+
severity TEXT NOT NULL CHECK (severity IN ('CRITICAL','HIGH','MEDIUM','LOW')),
|
|
800
|
+
description TEXT,
|
|
801
|
+
resolution_status TEXT NOT NULL DEFAULT 'pending' CHECK (resolution_status IN ('pending','resolved','wontfix')),
|
|
802
|
+
time_to_resolve_ms INTEGER, -- NULL enquanto pending
|
|
803
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
804
|
+
resolved_at TEXT
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
CREATE INDEX IF NOT EXISTS idx_vl_created ON validation_logs(created_at);
|
|
808
|
+
CREATE INDEX IF NOT EXISTS idx_vl_severity ON validation_logs(severity);
|
|
809
|
+
CREATE INDEX IF NOT EXISTS idx_vl_status ON validation_logs(resolution_status);
|
|
810
|
+
|
|
811
|
+
-- Tabela: api_failures
|
|
812
|
+
-- Alimentada pelo Server Tracking Agent (worker.js) quando um dispatch CAPI falha
|
|
813
|
+
CREATE TABLE IF NOT EXISTS api_failures (
|
|
814
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
815
|
+
platform TEXT NOT NULL, -- ex: 'meta', 'google', 'tiktok', 'linkedin'
|
|
816
|
+
event_name TEXT NOT NULL, -- ex: 'Purchase', 'Lead', 'ViewContent'
|
|
817
|
+
error_code TEXT, -- HTTP status ou código interno, ex: '429', 'TIMEOUT'
|
|
818
|
+
error_message TEXT,
|
|
819
|
+
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
820
|
+
final_status TEXT NOT NULL DEFAULT 'failed' CHECK (final_status IN ('failed','success','dlq')),
|
|
821
|
+
event_id TEXT, -- para correlação com events_log
|
|
822
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
CREATE INDEX IF NOT EXISTS idx_af_platform ON api_failures(platform);
|
|
826
|
+
CREATE INDEX IF NOT EXISTS idx_af_created ON api_failures(created_at);
|
|
827
|
+
CREATE INDEX IF NOT EXISTS idx_af_error_code ON api_failures(error_code);
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
### Como alimentar as tabelas
|
|
831
|
+
|
|
832
|
+
```javascript
|
|
833
|
+
// No Validator Agent — ao detectar um problema:
|
|
834
|
+
await env.DB.prepare(`
|
|
835
|
+
INSERT INTO validation_logs (agent_id, issue_type, severity, description)
|
|
836
|
+
VALUES (?, ?, ?, ?)
|
|
837
|
+
`).bind('validator-agent', issueType, severity, description).run();
|
|
838
|
+
|
|
839
|
+
// No worker.js — ao falhar um dispatch CAPI (já em ctx.waitUntil):
|
|
840
|
+
await env.DB.prepare(`
|
|
841
|
+
INSERT INTO api_failures (platform, event_name, error_code, error_message, retry_count, final_status, event_id)
|
|
842
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
843
|
+
`).bind(platform, eventName, errorCode, errorMessage, retryCount, finalStatus, eventId).run();
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
---
|
|
847
|
+
|
|
788
848
|
## 📊 CHECKLIST DE IMPLEMENTAÇÃO
|
|
789
849
|
|
|
790
850
|
### Coleta de Feedback
|
|
@@ -867,7 +927,7 @@ crons = [
|
|
|
867
927
|
```javascript
|
|
868
928
|
// Handler do ciclo completo
|
|
869
929
|
export async function scheduledFullCycle(event, env, ctx) {
|
|
870
|
-
const feedback = await collectFeedback();
|
|
930
|
+
const feedback = await collectFeedback(env);
|
|
871
931
|
const patterns = await analyzePatterns(feedback);
|
|
872
932
|
const rootCauses = await identifySystemicRootCauses(patterns);
|
|
873
933
|
const improvements = await prioritizeImprovements(rootCauses, feedback);
|