cdp-edge 1.5.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 +377 -0
- package/bin/cdp-edge.js +53 -0
- package/contracts/api-versions.json +368 -0
- package/dist/commands/analyze.js +52 -0
- package/dist/commands/infra.js +54 -0
- package/dist/commands/server.js +174 -0
- package/dist/commands/setup.js +123 -0
- package/dist/commands/validate.js +84 -0
- package/dist/index.js +12 -0
- package/package.json +72 -0
- package/templates/captura-de-lead.md +78 -0
- package/templates/captura-lead-evento-externo.md +99 -0
- package/templates/checkout-proprio.md +111 -0
- package/templates/multi-step-checkout.md +672 -0
- package/templates/pagina-obrigado.md +55 -0
- package/templates/pinterest/conversions-api-template.js +144 -0
- package/templates/pinterest/event-mappings.json +48 -0
- package/templates/pinterest/tag-template.js +28 -0
- package/templates/quiz-funnel.md +68 -0
- package/templates/reddit/conversions-api-template.js +205 -0
- package/templates/reddit/event-mappings.json +56 -0
- package/templates/reddit/pixel-template.js +19 -0
- package/templates/scenarios/behavior-engine.js +425 -0
- package/templates/scenarios/real-estate-logic.md +50 -0
- package/templates/scenarios/sales-page-logic.md +50 -0
- package/templates/trafego-direto.md +582 -0
- package/templates/webinar-registration.md +63 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Modelo: Página de Obrigado (Cloudflare Native)
|
|
2
|
+
|
|
3
|
+
Este modelo é destinado à página final do funil de vendas. O foco principal é a **deduplicação inteligente** e a garantia de que o evento de conversão foi registrado corretamente no banco de dados e nas APIs.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🏗️ ARQUITETURA TÉCNICA (Quantum Tier)
|
|
8
|
+
|
|
9
|
+
O rastreamento na página de obrigado segue dois caminhos:
|
|
10
|
+
1. **Obrigado de Lead**: Disparo direto via `cdpTrack.track()` se o lead ainda não foi marcado no servidor.
|
|
11
|
+
2. **Obrigado de Venda**: O evento de compra (`Purchase`) deve ser evitado se houver integração via Webhook, disparando apenas um `ViewContent` para análise de navegação.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 🛠️ PASSO 1: CONFIGURAÇÃO DO SITE
|
|
16
|
+
|
|
17
|
+
### 1.1 Obrigado de Lead (Formulário)
|
|
18
|
+
```javascript
|
|
19
|
+
<script>
|
|
20
|
+
// Dispara apenas se o lead ainda não foi marcado no Worker
|
|
21
|
+
cdpTrack.track('Lead_Success', {
|
|
22
|
+
source: 'thank_you_page'
|
|
23
|
+
});
|
|
24
|
+
</script>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 1.2 Obrigado de Venda (Purchase)
|
|
28
|
+
> [!IMPORTANT]
|
|
29
|
+
> Caso utilize **Webhooks (Ticto/Hotmart/Kiwify)**, a página de obrigado **NÃO** deve disparar o evento `Purchase`. O Worker fará o envio via servidor para evitar duplicidade e garantir 100% de atribuição.
|
|
30
|
+
|
|
31
|
+
```javascript
|
|
32
|
+
<script>
|
|
33
|
+
// Dispara evento de visualização para análise de funil
|
|
34
|
+
cdpTrack.track('ViewContent', {
|
|
35
|
+
content_name: 'Success_Page',
|
|
36
|
+
content_category: 'Sales'
|
|
37
|
+
});
|
|
38
|
+
</script>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 🖥️ PASSO 2: SERVIDOR (CLOUDFLARE WORKER)
|
|
44
|
+
|
|
45
|
+
O Worker realiza a verificação de duplicidade:
|
|
46
|
+
- **D1 Cross-Check**: Verifica se o `transaction_id` já existe no banco antes de enviar para Meta CAPI (v22.0) e TikTok (v1.3).
|
|
47
|
+
- **Match Quality**: Recupera os identificadores originais do banco de dados para enriquecer o evento.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## ✅ VALIDAÇÃO TÉCNICA
|
|
52
|
+
|
|
53
|
+
- **Deduplicação**: Verifique se não há disparos duplos de `Purchase` (um pelo site e outro pelo webhook).
|
|
54
|
+
- **Persistência**: O evento de sucesso deve estar registrado no log de eventos do D1.
|
|
55
|
+
- **Match Quality**: Conferir se os dados de atribuição (`fbp`, `fbc`) estão sendo enviados corretamente nas chamadas de servidor.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pinterest Conversions API Server Template — CDP Edge Quantum Tier
|
|
3
|
+
*
|
|
4
|
+
* Este template contém a função de envio para a Conversions API do Pinterest
|
|
5
|
+
* Uso: Incluir no worker.js gerado pelo Server Tracking Agent
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Função principal de envio para Pinterest Conversions API
|
|
10
|
+
* @param {Object} env - Variáveis de ambiente do Cloudflare Worker
|
|
11
|
+
* @param {Object} eventData - Dados do evento a ser enviado
|
|
12
|
+
* @returns {Promise<Object>} - Resposta da API
|
|
13
|
+
*/
|
|
14
|
+
export async function sendPinterestApi(env, eventData) {
|
|
15
|
+
const {
|
|
16
|
+
email, phone, userId, clientIp, userAgent, pageUrl,
|
|
17
|
+
eventId, value, currency, orderId, productName, productId
|
|
18
|
+
} = eventData;
|
|
19
|
+
|
|
20
|
+
// Verificar se as credenciais estão configuradas
|
|
21
|
+
if (!env.PINTEREST_ACCESS_TOKEN || !env.PINTEREST_AD_ACCOUNT_ID) {
|
|
22
|
+
console.warn('Pinterest Conversions API: Credenciais não configuradas');
|
|
23
|
+
return { success: false, error: 'MISSING_CREDENTIALS' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Função de hash SHA-256
|
|
27
|
+
async function sha256(str) {
|
|
28
|
+
if (!str) return undefined;
|
|
29
|
+
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str.toLowerCase().trim()));
|
|
30
|
+
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Normalizar evento — Pinterest usa nomes específicos
|
|
34
|
+
const pinterestEventMap = {
|
|
35
|
+
'PageView': 'pagevisit',
|
|
36
|
+
'Lead': 'lead',
|
|
37
|
+
'Purchase': 'checkout',
|
|
38
|
+
'AddToCart': 'addtocart',
|
|
39
|
+
'InitiateCheckout': 'checkout',
|
|
40
|
+
'ViewContent': 'pagevisit',
|
|
41
|
+
'CompleteRegistration': 'signup',
|
|
42
|
+
'Search': 'search',
|
|
43
|
+
};
|
|
44
|
+
const pinterestEvent = pinterestEventMap[eventName] || 'custom';
|
|
45
|
+
|
|
46
|
+
// User data com hashing
|
|
47
|
+
const userData = {};
|
|
48
|
+
if (email) userData.em = [await sha256(email)];
|
|
49
|
+
if (phone) userData.ph = [await sha256(phone.replace(/\D/g, ''))];
|
|
50
|
+
if (userId) userData.external_id = [await sha256(userId)];
|
|
51
|
+
if (clientIp) userData.client_ip_address = clientIp; // sem hash
|
|
52
|
+
if (userAgent) userData.client_user_agent = userAgent; // sem hash
|
|
53
|
+
|
|
54
|
+
// Payload da Conversions API
|
|
55
|
+
const payload = {
|
|
56
|
+
data: [{
|
|
57
|
+
event_name: pinterestEvent,
|
|
58
|
+
action_source: 'web',
|
|
59
|
+
event_time: Math.floor(Date.now() / 1000),
|
|
60
|
+
event_id: eventId, // deduplicação com browser tag
|
|
61
|
+
event_source_url: pageUrl || '',
|
|
62
|
+
user_data: userData,
|
|
63
|
+
custom_data: {
|
|
64
|
+
currency: currency || 'BRL',
|
|
65
|
+
value: value ? String(value) : '0',
|
|
66
|
+
order_id: orderId || undefined,
|
|
67
|
+
content_ids: productId ? [productId] : undefined,
|
|
68
|
+
content_name: productName || undefined,
|
|
69
|
+
content_type: 'product',
|
|
70
|
+
},
|
|
71
|
+
// Para lead: adicionar lead_type
|
|
72
|
+
...(eventName === 'Lead' ? { custom_data: { ...{}, lead_type: 'Newsletter' } } : {}),
|
|
73
|
+
}],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const resp = await fetch(
|
|
78
|
+
`https://api.pinterest.com/v5/ad_accounts/${env.PINTEREST_AD_ACCOUNT_ID}/events`,
|
|
79
|
+
{
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: {
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
'Authorization': `Bearer ${env.PINTEREST_ACCESS_TOKEN}`,
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify(payload),
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const result = await resp.json();
|
|
90
|
+
|
|
91
|
+
// Resposta de sucesso: { num_events_received: 1, num_events_processed: 1 }
|
|
92
|
+
if (result.num_events_received === 1 && result.num_events_processed === 1) {
|
|
93
|
+
return { success: true, result };
|
|
94
|
+
} else {
|
|
95
|
+
return { success: false, error: 'API_ERROR', result };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('Pinterest Conversions API Error:', error);
|
|
100
|
+
return { success: false, error: error.message };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Função de Enhanced Match — re-inicializa o pixel com dados hasheados
|
|
106
|
+
* @param {string} email - Email do usuário (opcional)
|
|
107
|
+
* @param {string} phone - Telefone do usuário (opcional)
|
|
108
|
+
*/
|
|
109
|
+
export async function reinitPinterestWithEnhancedMatch(email, phone) {
|
|
110
|
+
if (!email && !phone) return;
|
|
111
|
+
|
|
112
|
+
const hashedEmail = email ? await sha256Email(email) : '';
|
|
113
|
+
const hashedPhone = phone ? await sha256Phone(phone.replace(/\D/g, '')) : '';
|
|
114
|
+
|
|
115
|
+
// Re-load com dados hasheados para Enhanced Match
|
|
116
|
+
if (typeof pintrk !== 'undefined') {
|
|
117
|
+
pintrk('load', '{PINTEREST_TAG_ID}', {
|
|
118
|
+
em: hashedEmail,
|
|
119
|
+
ph: hashedPhone
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Funções de hash para Enhanced Match no browser
|
|
126
|
+
*/
|
|
127
|
+
export async function sha256Email(str) {
|
|
128
|
+
if (!str) return '';
|
|
129
|
+
const normalized = str.toLowerCase().trim();
|
|
130
|
+
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(normalized));
|
|
131
|
+
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function sha256Phone(str) {
|
|
135
|
+
if (!str) return '';
|
|
136
|
+
const normalized = str.replace(/\D/g, '').toLowerCase().trim();
|
|
137
|
+
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(normalized));
|
|
138
|
+
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export default {
|
|
142
|
+
sendPinterestApi,
|
|
143
|
+
reinitPinterestWithEnhancedMatch
|
|
144
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"platform": "pinterest",
|
|
3
|
+
"version": "v5",
|
|
4
|
+
"event_mappings": {
|
|
5
|
+
"pixel_to_native": {
|
|
6
|
+
"PageView": "pagevisit",
|
|
7
|
+
"Lead": "lead",
|
|
8
|
+
"Purchase": "checkout",
|
|
9
|
+
"AddToCart": "addtocart",
|
|
10
|
+
"InitiateCheckout": "checkout",
|
|
11
|
+
"ViewContent": "pagevisit",
|
|
12
|
+
"CompleteRegistration": "signup",
|
|
13
|
+
"Search": "search"
|
|
14
|
+
},
|
|
15
|
+
"native_names": {
|
|
16
|
+
"pagevisit": "Visualização de página",
|
|
17
|
+
"lead": "Formulário de lead",
|
|
18
|
+
"checkout": "Compra confirmada",
|
|
19
|
+
"addtocart": "Adicionar ao carrinho",
|
|
20
|
+
"signup": "Cadastro",
|
|
21
|
+
"search": "Busca"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"required_parameters": {
|
|
25
|
+
"pagevisit": ["line_items"],
|
|
26
|
+
"lead": ["lead_type"],
|
|
27
|
+
"addtocart": ["value", "currency", "line_items"],
|
|
28
|
+
"checkout": ["value", "currency", "order_id", "line_items"],
|
|
29
|
+
"signup": ["lead_type"],
|
|
30
|
+
"search": ["search_query"],
|
|
31
|
+
"watchvideo": []
|
|
32
|
+
},
|
|
33
|
+
"optional_parameters": {
|
|
34
|
+
"pagevisit": ["custom_data"],
|
|
35
|
+
"lead": ["custom_data"],
|
|
36
|
+
"addtocart": ["custom_data"],
|
|
37
|
+
"checkout": ["custom_data"],
|
|
38
|
+
"signup": ["custom_data"],
|
|
39
|
+
"search": ["custom_data"]
|
|
40
|
+
},
|
|
41
|
+
"enhanced_match_fields": {
|
|
42
|
+
"email": "em",
|
|
43
|
+
"phone": "ph",
|
|
44
|
+
"external_id": "external_id",
|
|
45
|
+
"client_ip_address": "client_ip_address",
|
|
46
|
+
"client_user_agent": "client_user_agent"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pinterest Pixel Browser Template — CDP Edge Quantum Tier
|
|
3
|
+
*
|
|
4
|
+
* Este template contém o código de inicialização do Pinterest Pixel
|
|
5
|
+
* Uso: Incluir no tracking.js gerado pelo Browser Tracking Agent
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const PINTEREST_TAG_TEMPLATE = `
|
|
9
|
+
<!-- Pinterest Pixel Base Code -->
|
|
10
|
+
<script>
|
|
11
|
+
!function(e){if(!window.pintrk){window.pintrk=function(){
|
|
12
|
+
window.pintrk.queue.push(Array.prototype.slice.call(arguments))};
|
|
13
|
+
var n=window.pintrk;n.queue=[],n.version="3.0";
|
|
14
|
+
var t=document.createElement("script");
|
|
15
|
+
t.async=!0,t.src=e;
|
|
16
|
+
var r=document.getElementsByTagName("script")[0];
|
|
17
|
+
r.parentNode.insertBefore(t,r)}}("https://s.pinimg.com/ct/core.js");
|
|
18
|
+
|
|
19
|
+
pintrk('load', '{PINTEREST_TAG_ID}', {
|
|
20
|
+
em: '<user_email_if_known>' // Enhanced Match — enviar email quando disponível
|
|
21
|
+
});
|
|
22
|
+
pintrk('page');
|
|
23
|
+
</script>
|
|
24
|
+
<noscript>
|
|
25
|
+
<img height="1" width="1" style="display:none;"
|
|
26
|
+
src="https://ct.pinterest.com/v3/?event=init&tid={PINTEREST_TAG_ID}&pd[em]=<hashed_email>&noscript=1" />
|
|
27
|
+
</noscript>
|
|
28
|
+
`;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Modelo: Quiz Funnel (Cloudflare Native)
|
|
2
|
+
|
|
3
|
+
Este modelo é destinado a funis de quiz, onde o usuário responde a uma série de perguntas antes de ser redirecionado para a oferta final. O rastreamento foca na progressão do usuário e na captura de dados intermediários.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🏗️ ARQUITETURA TÉCNICA (Quantum Tier)
|
|
8
|
+
|
|
9
|
+
O rastreamento segue a lógica de micro-eventos:
|
|
10
|
+
1. **Página**: Dispara um evento a cada resposta dada no quiz via `cdpTrack.track()`.
|
|
11
|
+
2. **Servidor (Worker)**: Recebe e armazena o progresso no banco D1.
|
|
12
|
+
3. **Database (D1)**: Mantém o histórico de respostas vinculado ao `track_user_id`.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 📘 EVENTOS PRINCIPAIS
|
|
17
|
+
|
|
18
|
+
| Evento | Gatilho | Dados Enviados |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| **QuizStart** | Início do quiz | `quiz_name`, `source` |
|
|
21
|
+
| **QuizAnswer** | Resposta a uma pergunta | `question`, `answer`, `step` |
|
|
22
|
+
| **QuizComplete** | Finalização do quiz | `result`, `completion_time` |
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 🛠️ PASSO 1: CONFIGURAÇÃO DO SITE
|
|
27
|
+
|
|
28
|
+
### 1.1 Rastreamento de Respostas
|
|
29
|
+
Integre este código na lógica de clique do seu quiz.
|
|
30
|
+
|
|
31
|
+
```javascript
|
|
32
|
+
// Exemplo de captura de resposta
|
|
33
|
+
function onResponder(pergunta, resposta, etapa) {
|
|
34
|
+
cdpTrack.track('QuizAnswer', {
|
|
35
|
+
question: pergunta,
|
|
36
|
+
answer: resposta,
|
|
37
|
+
step: etapa,
|
|
38
|
+
event_id: cdpTrack.generateId()
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 1.2 Finalização do Quiz
|
|
44
|
+
Disparar ao chegar no resultado final ou na página de captura pós-quiz.
|
|
45
|
+
|
|
46
|
+
```javascript
|
|
47
|
+
cdpTrack.track('QuizComplete', {
|
|
48
|
+
result: 'Perfil_A',
|
|
49
|
+
event_id: cdpTrack.generateId()
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## ⚡ PASSO 2: SERVIDOR (CLOUDFLARE WORKER)
|
|
56
|
+
|
|
57
|
+
O Worker realiza:
|
|
58
|
+
- **Agregação**: O `user_id` permite que todas as respostas sejam vinculadas a um único perfil no banco D1.
|
|
59
|
+
- **Enriquecimento**: Se o usuário deixar o e-mail no final, todas as respostas anteriores são associadas ao e-mail para a CAPI.
|
|
60
|
+
- **API Dispatch**: Envio de eventos customizados para Meta e TikTok para otimização de público.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## ✅ VALIDAÇÃO TÉCNICA
|
|
65
|
+
|
|
66
|
+
- **Persistência**: Verifique no banco D1 se a jornada do usuário está sendo gravada passo a passo.
|
|
67
|
+
- **Deduplicação**: O `event_id` único por resposta evita contagens duplicadas.
|
|
68
|
+
- **Match Quality**: A vinculação tardia do e-mail com as respostas iniciais aumenta a precisão da atribuição.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reddit Conversions API Server Template — CDP Edge Quantum Tier
|
|
3
|
+
*
|
|
4
|
+
* Este template contém a função de envio para a Conversions API do Reddit
|
|
5
|
+
* Uso: Incluir no worker.js gerado pelo Server Tracking Agent
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Função principal de envio para Reddit Conversions API
|
|
10
|
+
* @param {Object} env - Variáveis de ambiente do Cloudflare Worker
|
|
11
|
+
* @param {Object} eventData - Dados do evento a ser enviado
|
|
12
|
+
* @returns {Promise<Object>} - Resposta da API
|
|
13
|
+
*/
|
|
14
|
+
export async function sendRedditApi(env, eventData) {
|
|
15
|
+
const { email, phone, userId, clientIp, userAgent, pageUrl,
|
|
16
|
+
eventId, value, currency, itemCount, transactionId } = eventData;
|
|
17
|
+
|
|
18
|
+
// Verificar se as credenciais estão configuradas
|
|
19
|
+
if (!env.REDDIT_ACCESS_TOKEN || !env.REDDIT_AD_ACCOUNT_ID) {
|
|
20
|
+
console.warn('Reddit Conversions API: Credenciais não configuradas');
|
|
21
|
+
return { success: false, error: 'MISSING_CREDENTIALS' };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Função de hash SHA-256
|
|
25
|
+
async function sha256(str) {
|
|
26
|
+
if (!str) return undefined;
|
|
27
|
+
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str.toLowerCase().trim()));
|
|
28
|
+
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2,'0')).join('');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Mapeamento de eventos CDP Edge → Reddit
|
|
32
|
+
const redditEventMap = {
|
|
33
|
+
'PageView': 'PageVisit',
|
|
34
|
+
'ViewContent': 'ViewContent',
|
|
35
|
+
'Lead': 'Lead',
|
|
36
|
+
'Purchase': 'Purchase',
|
|
37
|
+
'AddToCart': 'AddToCart',
|
|
38
|
+
'CompleteRegistration': 'SignUp',
|
|
39
|
+
'Search': 'Search',
|
|
40
|
+
'InitiateCheckout': 'Purchase', // com conversionType BEGIN_CHECKOUT
|
|
41
|
+
};
|
|
42
|
+
const redditEvent = redditEventMap[eventName] || 'Custom';
|
|
43
|
+
|
|
44
|
+
// user object com SHA256
|
|
45
|
+
const user = {};
|
|
46
|
+
if (email) user.email = { value: await sha256(email) };
|
|
47
|
+
if (phone) user.phoneNumber = { value: await sha256(phone.replace(/\D/g,'')) };
|
|
48
|
+
if (userId) user.externalId = { value: await sha256(userId) };
|
|
49
|
+
if (clientIp) user.ipAddress = { value: clientIp }; // sem hash
|
|
50
|
+
if (userAgent) user.userAgent = { value: userAgent }; // sem hash
|
|
51
|
+
|
|
52
|
+
const event = {
|
|
53
|
+
event_at: new Date().toISOString(),
|
|
54
|
+
event_type: { tracking_type: redditEvent },
|
|
55
|
+
click_id: '', // Reddit click ID (rdt_cid) — se disponível da URL
|
|
56
|
+
event_metadata: {
|
|
57
|
+
currency: currency || 'BRL',
|
|
58
|
+
value_decimal: String(value || 0), // string com decimal
|
|
59
|
+
item_count: String(itemCount || 1),
|
|
60
|
+
transaction_id: transactionId || eventId,
|
|
61
|
+
conversion_id: eventId, // deduplicação
|
|
62
|
+
},
|
|
63
|
+
user,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Adicionar conversionType para InitiateCheckout
|
|
67
|
+
if (eventName === 'InitiateCheckout') {
|
|
68
|
+
event.event_type.conversion_type = 'BEGIN_CHECKOUT';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const payload = { events: [event] };
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const resp = await fetch(
|
|
75
|
+
`https://ads-api.reddit.com/api/v2.0/conversions/events/${env.REDDIT_AD_ACCOUNT_ID}`,
|
|
76
|
+
{
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: {
|
|
79
|
+
'Content-Type': 'application/json',
|
|
80
|
+
'Authorization': `Bearer ${env.REDDIT_ACCESS_TOKEN}`,
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify(payload),
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Resposta de sucesso: { status: 200, message: "Success" }
|
|
87
|
+
if (resp.ok) {
|
|
88
|
+
const result = await resp.json();
|
|
89
|
+
return { success: true, result };
|
|
90
|
+
} else {
|
|
91
|
+
return { success: false, error: `HTTP ${resp.status}` };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('Reddit Conversions API Error:', error);
|
|
96
|
+
return { success: false, error: error.message };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Função de Advanced Matching — re-inicializa o pixel com dados hasheados
|
|
102
|
+
* @param {Object} userData - Dados do usuário (email, phone, externalId)
|
|
103
|
+
*/
|
|
104
|
+
export function reinitRedditWithUserData(userData) {
|
|
105
|
+
const matchData = {};
|
|
106
|
+
if (userData.email) matchData.email = userData.email; // pixel faz hash
|
|
107
|
+
if (userData.phone) matchData.phoneNumber = userData.phone; // pixel faz hash
|
|
108
|
+
if (userData.externalId) matchData.externalId = userData.externalId;
|
|
109
|
+
|
|
110
|
+
rdt('init', '{REDDIT_PIXEL_ID}', matchData);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Funções de tracking de eventos (browser + server)
|
|
115
|
+
* Uso: Chamadas do tracking.js para eventos Reddit
|
|
116
|
+
*/
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Track de Lead
|
|
120
|
+
* @param {Object} data - Dados do formulário
|
|
121
|
+
*/
|
|
122
|
+
export async function trackRedditLead(data = {}) {
|
|
123
|
+
const eventId = generateEventId(); // reutilizar a função do tracking.js
|
|
124
|
+
|
|
125
|
+
// Browser: Reddit Pixel
|
|
126
|
+
if (typeof rdt !== 'undefined') {
|
|
127
|
+
if (data.email || data.phone) reinitRedditWithUserData(data);
|
|
128
|
+
rdt('track', 'Lead', { transactionId: eventId });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Servidor: Conversions API
|
|
132
|
+
await sendToServer('Lead', { ...data, eventId, redditEventId: eventId });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Track de Purchase
|
|
137
|
+
* @param {Object} data - Dados da compra
|
|
138
|
+
*/
|
|
139
|
+
export async function trackRedditPurchase(data = {}) {
|
|
140
|
+
const eventId = generateEventId();
|
|
141
|
+
|
|
142
|
+
if (typeof rdt !== 'undefined')
|
|
143
|
+
rdt('track', 'Purchase', {
|
|
144
|
+
value: data.value || 0,
|
|
145
|
+
currency: data.currency || 'BRL',
|
|
146
|
+
itemCount: data.itemCount ||1,
|
|
147
|
+
transactionId: data.transactionId || eventId,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await sendToServer('Purchase', { ...data, eventId, redditEventId: eventId });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Track de InitiateCheckout
|
|
155
|
+
* @param {Object} data - Dados do checkout iniciado
|
|
156
|
+
*/
|
|
157
|
+
export async function trackRedditInitiateCheckout(data = {}) {
|
|
158
|
+
const eventId = generateEventId();
|
|
159
|
+
|
|
160
|
+
if (typeof rdt !== 'undefined')
|
|
161
|
+
rdt('track', 'Purchase', {
|
|
162
|
+
conversionType: 'BEGIN_CHECKOUT',
|
|
163
|
+
value: data.value || 0,
|
|
164
|
+
currency: data.currency || 'BRL',
|
|
165
|
+
transactionId: eventId,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
await sendToServer('InitiateCheckout', { ...data, eventId });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Gerador de Event ID único
|
|
173
|
+
* Reutiliza a função do tracking.js ou gera novo
|
|
174
|
+
*/
|
|
175
|
+
function generateEventId() {
|
|
176
|
+
return crypto.randomUUID ? crypto.randomUUID() :
|
|
177
|
+
Date.now().toString(36) + Math.random().toString(36).slice(2);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Enviar dados para servidor via fetch
|
|
182
|
+
* @param {string} eventName - Nome do evento
|
|
183
|
+
* @param {Object} data - Dados completos do evento
|
|
184
|
+
*/
|
|
185
|
+
async function sendToServer(eventName, data) {
|
|
186
|
+
const serverUrl = '/api/track'; // ou URL configurada
|
|
187
|
+
|
|
188
|
+
await fetch(serverUrl, {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: { 'Content-Type': 'application/json' },
|
|
191
|
+
body: JSON.stringify({
|
|
192
|
+
event_name: eventName,
|
|
193
|
+
platform: 'reddit',
|
|
194
|
+
...data
|
|
195
|
+
})
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export default {
|
|
200
|
+
sendRedditApi,
|
|
201
|
+
reinitRedditWithUserData,
|
|
202
|
+
trackRedditLead,
|
|
203
|
+
trackRedditPurchase,
|
|
204
|
+
trackRedditInitiateCheckout
|
|
205
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"platform": "reddit",
|
|
3
|
+
"version": "v2.0",
|
|
4
|
+
"event_mappings": {
|
|
5
|
+
"pixel_to_native": {
|
|
6
|
+
"PageView": "PageVisit",
|
|
7
|
+
"ViewContent": "ViewContent",
|
|
8
|
+
"Lead": "Lead",
|
|
9
|
+
"Purchase": "Purchase",
|
|
10
|
+
"AddToCart": "AddToCart",
|
|
11
|
+
"AddToWishlist": "AddToWishlist",
|
|
12
|
+
"InitiateCheckout": "Purchase", // com conversionType: BEGIN_CHECKOUT
|
|
13
|
+
"CompleteRegistration": "SignUp",
|
|
14
|
+
"Search": "Search"
|
|
15
|
+
},
|
|
16
|
+
"native_names": {
|
|
17
|
+
"PageVisit": "Visualização de página",
|
|
18
|
+
"ViewContent": "Visualizar produto/conteúdo",
|
|
19
|
+
"Lead": "Formulário de lead",
|
|
20
|
+
"Purchase": "Compra confirmada",
|
|
21
|
+
"AddToCart": "Adicionar ao carrinho",
|
|
22
|
+
"AddToWishlist": "Adicionar à lista de desejos",
|
|
23
|
+
"SignUp": "Cadastro",
|
|
24
|
+
"Search": "Busca"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"required_parameters": {
|
|
28
|
+
"PageVisit": [],
|
|
29
|
+
"ViewContent": ["value", "currency", "itemCount"],
|
|
30
|
+
"Lead": [],
|
|
31
|
+
"Purchase": ["value", "currency", "itemCount", "transactionId"],
|
|
32
|
+
"AddToCart": ["value", "currency", "itemCount"],
|
|
33
|
+
"AddToWishlist": ["value", "currency", "itemCount"],
|
|
34
|
+
"InitiateCheckout": ["value", "currency"],
|
|
35
|
+
"SignUp": [],
|
|
36
|
+
"Search": []
|
|
37
|
+
},
|
|
38
|
+
"optional_parameters": {
|
|
39
|
+
"PageVisit": [],
|
|
40
|
+
"ViewContent": ["customData"],
|
|
41
|
+
"Lead": ["customData"],
|
|
42
|
+
"Purchase": ["customData"],
|
|
43
|
+
"AddToCart": ["customData"],
|
|
44
|
+
"AddToWishlist": ["customData"],
|
|
45
|
+
"InitiateCheckout": ["customData"],
|
|
46
|
+
"SignUp": ["customData"],
|
|
47
|
+
"Search": ["customData"]
|
|
48
|
+
},
|
|
49
|
+
"advanced_matching_fields": {
|
|
50
|
+
"email": "email",
|
|
51
|
+
"phoneNumber": "phoneNumber",
|
|
52
|
+
"externalId": "externalId",
|
|
53
|
+
"ipAddress": "ipAddress",
|
|
54
|
+
"userAgent": "userAgent"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reddit Pixel Browser Template — CDP Edge Quantum Tier
|
|
3
|
+
*
|
|
4
|
+
* Este template contém o código de inicialização do Reddit Pixel
|
|
5
|
+
* Uso: Incluir no tracking.js gerado pelo Browser Tracking Agent
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const REDDIT_PIXEL_TEMPLATE = `
|
|
9
|
+
<!-- Reddit Pixel Base Code -->
|
|
10
|
+
<script>
|
|
11
|
+
!function(w,d){if(!w.rdt){var p=w.rdt=function(){p.sendEvent?p.sendEvent.apply(p,arguments):p.callQueue.push(arguments);p.callQueue=[];var t=d.createElement('script');t.src='https://www.redditstatic.com/ads/v2.js',t.async=!0;var s=d.getElementsByTagName('script')[0];s.parentNode.insertBefore(t,s)}}(window,document);
|
|
12
|
+
rdt('init', '{REDDIT_PIXEL_ID}', {
|
|
13
|
+
optOut: false,
|
|
14
|
+
useDecimalCurrencyValues: true,
|
|
15
|
+
email: '', // preencher após captura (hashed automaticamente pelo pixel)
|
|
16
|
+
});
|
|
17
|
+
rdt('track', 'PageVisit');
|
|
18
|
+
</script>
|
|
19
|
+
`;
|