cdp-edge 2.0.0 → 2.0.2
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/contracts/api-versions.json +12 -8
- package/dist/commands/install.js +1 -2
- package/dist/commands/setup.js +1 -2
- package/extracted-skill/tracking-events-generator/agents/attribution-agent.md +23 -23
- package/extracted-skill/tracking-events-generator/agents/browser-tracking.md +172 -72
- 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/google-agent.md +118 -0
- package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +90 -4
- package/extracted-skill/tracking-events-generator/agents/intelligence-scheduling.md +8 -641
- 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 +61 -18
- package/extracted-skill/tracking-events-generator/agents/memory-agent.md +98 -0
- 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/security-enterprise-agent.md +137 -28
- package/extracted-skill/tracking-events-generator/agents/server-tracking.md +7 -8
- 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 +100 -0
- 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 +140 -25
- package/extracted-skill/tracking-events-generator/contracts/api-versions.json +12 -8
- package/package.json +2 -2
- package/server-edge-tracker/worker.js +53 -8
|
@@ -12,6 +12,38 @@ Você é o **Agente de Integração CRM do CDP Edge**. Sua responsabilidade: **c
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
+
## 🔗 FLUXO DE ATIVAÇÃO (Como este agente é chamado)
|
|
16
|
+
|
|
17
|
+
O CRM Integration Agent é ativado pelo **Webhook Agent** após confirmação de compra ou captura de lead:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Webhook Agent (purchase confirmado)
|
|
21
|
+
└─► ctx.waitUntil(syncToCRM(env, 'purchase', payload))
|
|
22
|
+
│
|
|
23
|
+
├─ Criar/atualizar contato no CRM com status 'customer'
|
|
24
|
+
├─ Criar negócio/oportunidade com valor da compra
|
|
25
|
+
└─ Atualizar D1 com CRM_CONTACT_ID para rastreamento futuro
|
|
26
|
+
|
|
27
|
+
Webhook Agent (lead capturado via /track)
|
|
28
|
+
└─► ctx.waitUntil(syncToCRM(env, 'lead', payload))
|
|
29
|
+
│
|
|
30
|
+
├─ Criar contato no CRM com status 'lead'
|
|
31
|
+
└─ Registrar no D1 para cruzamento futuro
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Assinatura da função exportada (injetada no Worker):**
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
// Injetada no worker.js pelo CRM Integration Agent
|
|
38
|
+
export async function syncToCRM(env, eventType, payload) {
|
|
39
|
+
// eventType: 'lead' | 'purchase' | 'checkout_abandoned'
|
|
40
|
+
// payload: { email, name, product, value, order_id, phone }
|
|
41
|
+
// Retorna: { success: boolean, crm_contact_id: string | null }
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
15
47
|
## 🎯 OBJETIVO PRINCIPAL
|
|
16
48
|
|
|
17
49
|
Implementar **integração bidirecional** entre CDP Edge D1 e CRMs externos, permitindo que dados de tracking (leads, purchases, user journeys) fluam automaticamente para sistemas de vendas, marketing e atendimento, eliminando importações manuais e maximizando conversão.
|
|
@@ -612,8 +644,8 @@ export const PURCHASE_FIELD_MAPPINGS = {
|
|
|
612
644
|
|
|
613
645
|
```javascript
|
|
614
646
|
// Sincronizar leads do D1 para CRM em batch
|
|
615
|
-
export async function syncLeadsToCRM(hours = 24, batchSize = 50) {
|
|
616
|
-
const leads = await DB.prepare(`
|
|
647
|
+
export async function syncLeadsToCRM(env, hours = 24, batchSize = 50) {
|
|
648
|
+
const leads = await env.DB.prepare(`
|
|
617
649
|
SELECT * FROM leads
|
|
618
650
|
WHERE created_at > datetime('now', '-${hours} hours')
|
|
619
651
|
ORDER BY created_at DESC
|
|
@@ -699,8 +731,8 @@ async function sendLeadsBatchToCRM(leadsBatch) {
|
|
|
699
731
|
};
|
|
700
732
|
}
|
|
701
733
|
|
|
702
|
-
async function markLeadAsSynced(leadId) {
|
|
703
|
-
await DB.prepare(`
|
|
734
|
+
async function markLeadAsSynced(leadId, env) {
|
|
735
|
+
await env.DB.prepare(`
|
|
704
736
|
UPDATE leads SET crm_synced = 1, crm_synced_at = datetime('now')
|
|
705
737
|
WHERE id = ?
|
|
706
738
|
`).bind(leadId).run();
|
|
@@ -711,8 +743,8 @@ async function markLeadAsSynced(leadId) {
|
|
|
711
743
|
|
|
712
744
|
```javascript
|
|
713
745
|
// Sincronizar compras do D1 para CRM em batch
|
|
714
|
-
export async function syncPurchasesToCRM(hours = 24, batchSize = 20) {
|
|
715
|
-
const purchases = await DB.prepare(`
|
|
746
|
+
export async function syncPurchasesToCRM(env, hours = 24, batchSize = 20) {
|
|
747
|
+
const purchases = await env.DB.prepare(`
|
|
716
748
|
SELECT p.*, l.email
|
|
717
749
|
FROM purchases p
|
|
718
750
|
LEFT JOIN leads l ON p.lead_id = l.id
|
|
@@ -800,8 +832,8 @@ async function sendPurchasesBatchToCRM(purchasesBatch) {
|
|
|
800
832
|
};
|
|
801
833
|
}
|
|
802
834
|
|
|
803
|
-
async function markPurchaseAsSynced(purchaseId) {
|
|
804
|
-
await DB.prepare(`
|
|
835
|
+
async function markPurchaseAsSynced(purchaseId, env) {
|
|
836
|
+
await env.DB.prepare(`
|
|
805
837
|
UPDATE purchases SET crm_synced = 1, crm_synced_at = datetime('now')
|
|
806
838
|
WHERE id = ?
|
|
807
839
|
`).bind(purchaseId).run();
|
|
@@ -853,8 +885,8 @@ export async function handleCRMWebhook(request, env) {
|
|
|
853
885
|
}
|
|
854
886
|
}
|
|
855
887
|
|
|
856
|
-
async function updateLeadStageFromCRM(payload) {
|
|
857
|
-
await DB.prepare(`
|
|
888
|
+
async function updateLeadStageFromCRM(payload, env) {
|
|
889
|
+
await env.DB.prepare(`
|
|
858
890
|
UPDATE leads SET crm_stage = ?, crm_stage_updated_at = datetime('now')
|
|
859
891
|
WHERE email = ?
|
|
860
892
|
`).bind(payload.stage, payload.email).run();
|
|
@@ -862,8 +894,8 @@ async function updateLeadStageFromCRM(payload) {
|
|
|
862
894
|
console.log(`Stage do lead ${payload.email} atualizado para: ${payload.stage}`);
|
|
863
895
|
}
|
|
864
896
|
|
|
865
|
-
async function updateDealStatusFromCRM(payload) {
|
|
866
|
-
await DB.prepare(`
|
|
897
|
+
async function updateDealStatusFromCRM(payload, env) {
|
|
898
|
+
await env.DB.prepare(`
|
|
867
899
|
UPDATE purchases SET crm_deal_status = ?, crm_deal_status_updated_at = datetime('now')
|
|
868
900
|
WHERE transaction_id = ?
|
|
869
901
|
`).bind(payload.status, payload.transaction_id).run();
|
|
@@ -871,8 +903,8 @@ async function updateDealStatusFromCRM(payload) {
|
|
|
871
903
|
console.log(`Status da compra ${payload.transaction_id} atualizado para: ${payload.status}`);
|
|
872
904
|
}
|
|
873
905
|
|
|
874
|
-
async function updateLeadScoreFromCRM(payload) {
|
|
875
|
-
await DB.prepare(`
|
|
906
|
+
async function updateLeadScoreFromCRM(payload, env) {
|
|
907
|
+
await env.DB.prepare(`
|
|
876
908
|
UPDATE leads SET lead_score = ?, lead_score_updated_at = datetime('now')
|
|
877
909
|
WHERE email = ?
|
|
878
910
|
`).bind(payload.lead_score, payload.email).run();
|
|
@@ -979,7 +1011,7 @@ export async function registerCRMWebhooks(crmType, webhookUrl) {
|
|
|
979
1011
|
};
|
|
980
1012
|
|
|
981
1013
|
// Salvar configuração no D1
|
|
982
|
-
await DB.prepare(`
|
|
1014
|
+
await env.DB.prepare(`
|
|
983
1015
|
INSERT INTO crm_webhook_config (webhook_url, crm_type, events, signature_enabled)
|
|
984
1016
|
VALUES (?, ?, ?, ?)
|
|
985
1017
|
`).bind(
|
|
@@ -994,7 +1026,7 @@ export async function registerCRMWebhooks(crmType, webhookUrl) {
|
|
|
994
1026
|
|
|
995
1027
|
// Endpoint de saúde da integração
|
|
996
1028
|
export async function handleCRMHealthCheck(request, env) {
|
|
997
|
-
const stats = await DB.prepare(`
|
|
1029
|
+
const stats = await env.DB.prepare(`
|
|
998
1030
|
SELECT
|
|
999
1031
|
COUNT(*) as total_leads,
|
|
1000
1032
|
COUNT(CASE WHEN crm_synced = 1 THEN 1 END) as synced_leads,
|
|
@@ -96,16 +96,16 @@ Definir o Dashboard como um **Centro de Comando de Dados** que equilibra:
|
|
|
96
96
|
**Implementação:**
|
|
97
97
|
```javascript
|
|
98
98
|
// Carregar dados do cache
|
|
99
|
-
const fetchMetrics = async (forceRefresh = false) => {
|
|
99
|
+
const fetchMetrics = async (env, forceRefresh = false) => {
|
|
100
100
|
const cacheKey = 'dashboard_metrics';
|
|
101
|
-
const cached = await
|
|
101
|
+
const cached = await env.GEO_CACHE.get(cacheKey);
|
|
102
102
|
|
|
103
103
|
if (cached && !forceRefresh) {
|
|
104
104
|
return JSON.parse(cached);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
// Cache miss ou refresh forçado — consultar D1
|
|
108
|
-
const metrics = await DB.prepare(`
|
|
108
|
+
const metrics = await env.DB.prepare(`
|
|
109
109
|
SELECT
|
|
110
110
|
AVG(heat_score) as avg_heat,
|
|
111
111
|
COUNT(DISTINCT fingerprint) as total_users,
|
|
@@ -115,7 +115,7 @@ const fetchMetrics = async (forceRefresh = false) => {
|
|
|
115
115
|
`).all();
|
|
116
116
|
|
|
117
117
|
// Salvar no KV com TTL de 1 hora
|
|
118
|
-
await
|
|
118
|
+
await env.GEO_CACHE.put(cacheKey, JSON.stringify(metrics), { expirationTtl: 3600 });
|
|
119
119
|
|
|
120
120
|
return metrics;
|
|
121
121
|
};
|
|
@@ -217,13 +217,13 @@ export const DASHBOARD_API = {
|
|
|
217
217
|
};
|
|
218
218
|
|
|
219
219
|
// HANDLER DE SINCronizaÇÃO (atualização de cache)
|
|
220
|
-
export async function invalidateCache(domain, pattern) {
|
|
220
|
+
export async function invalidateCache(domain, pattern, env) {
|
|
221
221
|
const deletePattern = `${domain}:${pattern}`;
|
|
222
222
|
|
|
223
223
|
// Deletar do KV
|
|
224
|
-
const keys = await
|
|
224
|
+
const keys = await env.GEO_CACHE.list({ prefix: deletePattern });
|
|
225
225
|
for (const key of keys.keys) {
|
|
226
|
-
await
|
|
226
|
+
await env.GEO_CACHE.delete(key);
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
// Retornar contagem
|
|
@@ -78,7 +78,7 @@ crons = ["0 2 * * 7", "0 3 1 * *"]
|
|
|
78
78
|
- **ID:** `SEU_D1_DATABASE_ID`
|
|
79
79
|
- **Região:** ENAM (US East)
|
|
80
80
|
- **Engine:** SQLite (Cloudflare D1)
|
|
81
|
-
- **Tabelas ativas:**
|
|
81
|
+
- **Tabelas ativas:** 24 (core: 13, migrate-v6: 1, Enterprise Fases 1-4: 10)
|
|
82
82
|
|
|
83
83
|
### Schema Completo (Produção)
|
|
84
84
|
|
|
@@ -259,7 +259,7 @@ CREATE INDEX IF NOT EXISTS idx_wa_ctwa_clid ON whatsapp_contacts(ctwa_clid);
|
|
|
259
259
|
CREATE INDEX IF NOT EXISTS idx_wa_created_at ON whatsapp_contacts(created_at);
|
|
260
260
|
```
|
|
261
261
|
> **Migration:** `migrate-v6.sql` — aplicar com `wrangler d1 execute cdp-edge-db --file=migrate-v6.sql --remote`
|
|
262
|
-
> **Tabelas ativas após v6:**
|
|
262
|
+
> **Tabelas ativas após v6:** 14 (core: 13 + whatsapp_contacts)
|
|
263
263
|
|
|
264
264
|
---
|
|
265
265
|
|
|
@@ -313,9 +313,9 @@ ctx.waitUntil(
|
|
|
313
313
|
|
|
314
314
|
---
|
|
315
315
|
|
|
316
|
-
## 📬 BINDING 2 — QUEUES (`env.
|
|
316
|
+
## 📬 BINDING 2 — QUEUES (`env.RETRY_QUEUE`) — ✅ Configurado
|
|
317
317
|
|
|
318
|
-
> **Status:**
|
|
318
|
+
> **Status:** Ativo em produção — `wrangler.toml` com `RETRY_QUEUE` binding e consumer configurados.
|
|
319
319
|
|
|
320
320
|
### Configuração no `wrangler.toml`
|
|
321
321
|
```toml
|
|
@@ -387,9 +387,9 @@ Queue Consumer (assíncrono)
|
|
|
387
387
|
|
|
388
388
|
---
|
|
389
389
|
|
|
390
|
-
## 🗂️ BINDING 3 — KV NAMESPACE (`env.GEO_CACHE`) —
|
|
390
|
+
## 🗂️ BINDING 3 — KV NAMESPACE (`env.GEO_CACHE`) — ✅ Configurado
|
|
391
391
|
|
|
392
|
-
> **Status:**
|
|
392
|
+
> **Status:** Ativo em produção — usado para geo cache, fraud blocklist, fraud velocity counters e A/B test cache.
|
|
393
393
|
|
|
394
394
|
### Configuração no `wrangler.toml`
|
|
395
395
|
```toml
|
|
@@ -548,7 +548,7 @@ async function runIntelligenceAgent(cron, env) {
|
|
|
548
548
|
| `META_TEST_CODE` | ⚠️ Reconfigurar | Só testes | meta-agent |
|
|
549
549
|
| `META_AD_ACCOUNT_ID` | ⚠️ Reconfigurar | Customer Match | meta-agent |
|
|
550
550
|
| `META_AUDIENCE_ID` | ⚠️ Reconfigurar | Customer Match | meta-agent |
|
|
551
|
-
| `
|
|
551
|
+
| `WHATSAPP_ACCESS_TOKEN` | ⚠️ Reconfigurar | WhatsApp | whatsapp-agent |
|
|
552
552
|
| `WHATSAPP_PHONE_NUMBER_ID` | ⚠️ Reconfigurar | WhatsApp | whatsapp-agent |
|
|
553
553
|
| `RESEND_API_KEY` | ⚠️ Reconfigurar | Email | email-agent |
|
|
554
554
|
| `RESEND_FROM_EMAIL` | ⚠️ Reconfigurar | Email | email-agent |
|
|
@@ -560,7 +560,7 @@ async function runIntelligenceAgent(cron, env) {
|
|
|
560
560
|
wrangler secret put META_ACCESS_TOKEN
|
|
561
561
|
wrangler secret put GA4_API_SECRET
|
|
562
562
|
wrangler secret put TIKTOK_ACCESS_TOKEN
|
|
563
|
-
wrangler secret put
|
|
563
|
+
wrangler secret put WHATSAPP_ACCESS_TOKEN
|
|
564
564
|
wrangler secret put WHATSAPP_PHONE_NUMBER_ID
|
|
565
565
|
wrangler secret put RESEND_API_KEY
|
|
566
566
|
wrangler secret put RESEND_FROM_EMAIL
|
|
@@ -56,9 +56,9 @@ export function logWorker(level, category, message, context = {}) {
|
|
|
56
56
|
console.log(`[${level}] [${category}] ${message}`, JSON.stringify(context));
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
async function persistLogEntry(logEntry) {
|
|
59
|
+
async function persistLogEntry(env, logEntry) {
|
|
60
60
|
try {
|
|
61
|
-
await DB.prepare(`
|
|
61
|
+
await env.DB.prepare(`
|
|
62
62
|
INSERT INTO worker_logs (timestamp, level, category, message, context, session_id, request_id)
|
|
63
63
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
64
64
|
`).bind(
|
|
@@ -82,7 +82,7 @@ async function dispatchEventToMeta(event, userContext) {
|
|
|
82
82
|
try {
|
|
83
83
|
const response = await fetch('https://graph.facebook.com/v22.0/events', {
|
|
84
84
|
method: 'POST',
|
|
85
|
-
headers: { 'Authorization': `Bearer ${META_ACCESS_TOKEN}` },
|
|
85
|
+
headers: { 'Authorization': `Bearer ${env.META_ACCESS_TOKEN}` },
|
|
86
86
|
body: JSON.stringify(event)
|
|
87
87
|
});
|
|
88
88
|
|
|
@@ -221,7 +221,7 @@ cdpTrack.track = function(eventName, params) {
|
|
|
221
221
|
};
|
|
222
222
|
|
|
223
223
|
// Enviar para Worker
|
|
224
|
-
fetch('/
|
|
224
|
+
fetch('/track', {
|
|
225
225
|
method: 'POST',
|
|
226
226
|
headers: { 'Content-Type': 'application/json' },
|
|
227
227
|
body: JSON.stringify(eventPayload)
|
|
@@ -331,7 +331,7 @@ export async function handleDebugRequest(request, env) {
|
|
|
331
331
|
}
|
|
332
332
|
|
|
333
333
|
async function getApiHealth(platform) {
|
|
334
|
-
const recentFailures = await DB.prepare(`
|
|
334
|
+
const recentFailures = await env.DB.prepare(`
|
|
335
335
|
SELECT
|
|
336
336
|
COUNT(*) as failure_count,
|
|
337
337
|
MAX(created_at) as last_failure_at
|
|
@@ -339,7 +339,7 @@ async function getApiHealth(platform) {
|
|
|
339
339
|
WHERE platform = ? AND created_at > datetime('now', '-1 hour')
|
|
340
340
|
`).bind(platform).get();
|
|
341
341
|
|
|
342
|
-
const recentSuccesses = await DB.prepare(`
|
|
342
|
+
const recentSuccesses = await env.DB.prepare(`
|
|
343
343
|
SELECT COUNT(*) as success_count
|
|
344
344
|
FROM events_log
|
|
345
345
|
WHERE platform = ? AND status = 'success' AND created_at > datetime('now', '-1 hour')
|
|
@@ -394,7 +394,7 @@ export async function handleHealthCheck(request, env) {
|
|
|
394
394
|
}
|
|
395
395
|
|
|
396
396
|
async function getSimpleApiHealth(platform) {
|
|
397
|
-
const lastSuccess = await DB.prepare(`
|
|
397
|
+
const lastSuccess = await env.DB.prepare(`
|
|
398
398
|
SELECT created_at
|
|
399
399
|
FROM events_log
|
|
400
400
|
WHERE platform = ? AND status = 'success'
|
|
@@ -402,7 +402,7 @@ async function getSimpleApiHealth(platform) {
|
|
|
402
402
|
LIMIT 1
|
|
403
403
|
`).bind(platform).get();
|
|
404
404
|
|
|
405
|
-
const lastFailure = await DB.prepare(`
|
|
405
|
+
const lastFailure = await env.DB.prepare(`
|
|
406
406
|
SELECT created_at
|
|
407
407
|
FROM api_failures
|
|
408
408
|
WHERE platform = ?
|
|
@@ -433,7 +433,7 @@ export async function handleEventsLogRequest(request, env) {
|
|
|
433
433
|
const limit = parseInt(url.searchParams.get('limit') || '50');
|
|
434
434
|
const hours = parseInt(url.searchParams.get('hours') || '24');
|
|
435
435
|
|
|
436
|
-
const events = await DB.prepare(`
|
|
436
|
+
const events = await env.DB.prepare(`
|
|
437
437
|
SELECT
|
|
438
438
|
event_name,
|
|
439
439
|
platform,
|
|
@@ -485,7 +485,7 @@ export async function handleEventsLogRequest(request, env) {
|
|
|
485
485
|
|
|
486
486
|
- [ ] **Envio de eventos para Worker:**
|
|
487
487
|
- [ ] Verificar Network tab em Developer Tools
|
|
488
|
-
- [ ] Confirmar requisição para `/
|
|
488
|
+
- [ ] Confirmar requisição para `/track`
|
|
489
489
|
- [ ] Verificar status da resposta (deve ser 200 OK)
|
|
490
490
|
- [ ] Verificar payload enviado (deve conter event_name, params, timestamp)
|
|
491
491
|
|
|
@@ -1033,7 +1033,7 @@ export async function handleEventsLogRequest(request, env) {
|
|
|
1033
1033
|
const limit = parseInt(url.searchParams.get('limit') || '50');
|
|
1034
1034
|
const hours = parseInt(url.searchParams.get('hours') || '24');
|
|
1035
1035
|
|
|
1036
|
-
const events = await DB.prepare(`
|
|
1036
|
+
const events = await env.DB.prepare(`
|
|
1037
1037
|
SELECT event_name, platform, status, error_message, created_at
|
|
1038
1038
|
FROM events_log
|
|
1039
1039
|
WHERE created_at > datetime('now', '-${hours} hours')
|
|
@@ -1057,7 +1057,7 @@ export async function handleBrowserLogs(request, env) {
|
|
|
1057
1057
|
|
|
1058
1058
|
// Persistir logs do browser no D1
|
|
1059
1059
|
for (const log of logs) {
|
|
1060
|
-
await DB.prepare(`
|
|
1060
|
+
await env.DB.prepare(`
|
|
1061
1061
|
INSERT INTO browser_logs (timestamp, level, category, message, context, session_id, page_url)
|
|
1062
1062
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1063
1063
|
`).bind(
|
|
@@ -1182,7 +1182,7 @@ window.pbDebugLogger = logger;
|
|
|
1182
1182
|
**Possíveis Causas:**
|
|
1183
1183
|
|
|
1184
1184
|
1. **Evento não está sendo enviado para a plataforma**
|
|
1185
|
-
- Verificar se `/
|
|
1185
|
+
- Verificar se `/track` está recebendo o evento
|
|
1186
1186
|
- Consultar `/api/events-log` para ver se evento foi processado
|
|
1187
1187
|
- Verificar logs do Worker: `wrangler tail`
|
|
1188
1188
|
|
|
@@ -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`
|