cdp-edge 1.17.0 → 1.18.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/contracts/api-versions.json +12 -8
- package/dist/commands/install.js +186 -0
- package/dist/commands/setup.js +18 -1
- package/extracted-skill/tracking-events-generator/agents/attribution-agent.md +23 -23
- package/extracted-skill/tracking-events-generator/agents/bing-agent.md +8 -0
- 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 +56 -16
- package/extracted-skill/tracking-events-generator/agents/dashboard-agent.md +7 -7
- package/extracted-skill/tracking-events-generator/agents/database-agent.md +16 -8
- package/extracted-skill/tracking-events-generator/agents/debug-agent.md +13 -13
- package/extracted-skill/tracking-events-generator/agents/devops-agent.md +32 -7
- package/extracted-skill/tracking-events-generator/agents/domain-setup-agent.md +8 -0
- 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 +126 -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 +116 -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 +71 -34
- package/extracted-skill/tracking-events-generator/agents/memory-agent.md +127 -2
- package/extracted-skill/tracking-events-generator/agents/meta-agent.md +8 -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/pinterest-agent.md +8 -0
- package/extracted-skill/tracking-events-generator/agents/r2-setup-agent.md +8 -0
- package/extracted-skill/tracking-events-generator/agents/reddit-agent.md +8 -0
- package/extracted-skill/tracking-events-generator/agents/security-enterprise-agent.md +137 -28
- package/extracted-skill/tracking-events-generator/agents/server-tracking.md +8 -8
- package/extracted-skill/tracking-events-generator/agents/spotify-agent.md +8 -0
- package/extracted-skill/tracking-events-generator/agents/tiktok-agent.md +71 -0
- package/extracted-skill/tracking-events-generator/agents/tracking-plan-agent.md +100 -5
- package/extracted-skill/tracking-events-generator/agents/validator-agent.md +4 -0
- package/extracted-skill/tracking-events-generator/agents/webhook-agent.md +108 -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 +23 -15
- 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
|
@@ -89,39 +89,152 @@ const _gbraid = _urlParams.get('gbraid') || ''; // App campaigns (privacy)
|
|
|
89
89
|
**ATENÇÃO wbraid/gbraid**: São os click IDs para campanhas YouTube em iOS (pós ATT).
|
|
90
90
|
Nunca hashear — enviar como texto plano para Google Ads API.
|
|
91
91
|
|
|
92
|
-
### 2. Rastreamento de vídeo YouTube na página
|
|
92
|
+
### 2. Rastreamento de vídeo YouTube na página — YouTube IFrame API
|
|
93
93
|
|
|
94
|
-
O `behavior-engine.js` já implementa rastreamento de vídeos via YouTube IFrame API.
|
|
95
94
|
Para usar, o iframe deve ter `enablejsapi=1`:
|
|
96
95
|
|
|
97
96
|
```html
|
|
98
|
-
<!-- Embed YouTube com JS API habilitada -->
|
|
97
|
+
<!-- Embed YouTube com JS API habilitada (obrigatório) -->
|
|
99
98
|
<iframe
|
|
100
99
|
id="video-tour-imovel"
|
|
101
|
-
src="https://www.youtube.com/embed/VIDEO_ID?enablejsapi=1"
|
|
100
|
+
src="https://www.youtube.com/embed/VIDEO_ID?enablejsapi=1&origin=https://seudominio.com.br"
|
|
102
101
|
allow="autoplay"
|
|
103
102
|
></iframe>
|
|
104
103
|
```
|
|
105
104
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
#### Implementação real do YouTube IFrame API listener
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
/**
|
|
109
|
+
* YouTube IFrame API Listener — injeta no behavior-engine.js ou tracking.js
|
|
110
|
+
* Rastreia: video_start, video_25, video_50, video_75, video_complete
|
|
111
|
+
* Dispara via cdpTrack.track() para o Worker → GA4 MP + demais plataformas
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
// Carregar YouTube IFrame API (uma vez por página)
|
|
115
|
+
(function initYouTubeTracking() {
|
|
116
|
+
if (window._ytTrackingInitialized) return;
|
|
117
|
+
window._ytTrackingInitialized = true;
|
|
118
|
+
|
|
119
|
+
// Mapa de iframes já trackeados
|
|
120
|
+
const trackedPlayers = new Map();
|
|
121
|
+
|
|
122
|
+
// Injetar API script do YouTube (não carrega 2x se já existe)
|
|
123
|
+
if (!document.getElementById('youtube-iframe-api')) {
|
|
124
|
+
const tag = document.createElement('script');
|
|
125
|
+
tag.id = 'youtube-iframe-api';
|
|
126
|
+
tag.src = 'https://www.youtube.com/iframe_api';
|
|
127
|
+
document.head.appendChild(tag);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Callback global chamado pelo YouTube quando API estiver pronta
|
|
131
|
+
window.onYouTubeIframeAPIReady = function() {
|
|
132
|
+
// Auto-detectar todos os iframes com enablejsapi=1
|
|
133
|
+
document.querySelectorAll('iframe[src*="youtube.com/embed"]').forEach(iframe => {
|
|
134
|
+
if (trackedPlayers.has(iframe.id)) return;
|
|
135
|
+
|
|
136
|
+
const videoTitle = iframe.title || iframe.id || 'YouTube Video';
|
|
137
|
+
|
|
138
|
+
const player = new YT.Player(iframe.id, {
|
|
139
|
+
events: {
|
|
140
|
+
onStateChange: (event) => handlePlayerStateChange(event, player, videoTitle),
|
|
141
|
+
onReady: (event) => handlePlayerReady(event, player, videoTitle)
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
trackedPlayers.set(iframe.id, { player, milestone: new Set() });
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Se API já carregada (SPA reload), inicializar diretamente
|
|
150
|
+
if (typeof YT !== 'undefined' && YT.Player) {
|
|
151
|
+
window.onYouTubeIframeAPIReady();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function handlePlayerReady(event, player, videoTitle) {
|
|
155
|
+
// Iniciar polling de progresso
|
|
156
|
+
const iframeId = player.getIframe().id;
|
|
157
|
+
const state = trackedPlayers.get(iframeId);
|
|
158
|
+
|
|
159
|
+
const interval = setInterval(() => {
|
|
160
|
+
if (!player.getDuration) return;
|
|
161
|
+
const duration = player.getDuration();
|
|
162
|
+
const current = player.getCurrentTime();
|
|
163
|
+
if (duration <= 0) return;
|
|
164
|
+
|
|
165
|
+
const percent = Math.floor((current / duration) * 100);
|
|
166
|
+
|
|
167
|
+
// Disparar milestones: 25, 50, 75 (100% é coberto pelo estado ENDED)
|
|
168
|
+
const milestoneEvents = { 25: 'video_25', 50: 'video_50', 75: 'video_75' };
|
|
169
|
+
[25, 50, 75].forEach(milestone => {
|
|
170
|
+
if (percent >= milestone && !state.milestone.has(milestone)) {
|
|
171
|
+
state.milestone.add(milestone);
|
|
172
|
+
|
|
173
|
+
window.cdpTrack?.track(milestoneEvents[milestone], {
|
|
174
|
+
content_name: videoTitle,
|
|
175
|
+
video_percent: milestone,
|
|
176
|
+
video_duration: Math.round(duration),
|
|
177
|
+
video_provider: 'youtube',
|
|
178
|
+
value: 0,
|
|
179
|
+
currency: 'BRL'
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}, 1000); // checar a cada 1s
|
|
184
|
+
|
|
185
|
+
state.progressInterval = interval;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function handlePlayerStateChange(event, player, videoTitle) {
|
|
189
|
+
const iframeId = player.getIframe().id;
|
|
190
|
+
const state = trackedPlayers.get(iframeId);
|
|
191
|
+
|
|
192
|
+
// YT.PlayerState: PLAYING=1, PAUSED=2, ENDED=0, BUFFERING=3
|
|
193
|
+
switch (event.data) {
|
|
194
|
+
case YT.PlayerState.PLAYING:
|
|
195
|
+
if (!state.started) {
|
|
196
|
+
state.started = true;
|
|
197
|
+
window.cdpTrack?.track('video_start', {
|
|
198
|
+
content_name: videoTitle,
|
|
199
|
+
video_duration: Math.round(player.getDuration() || 0),
|
|
200
|
+
video_provider: 'youtube'
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
|
|
205
|
+
case YT.PlayerState.ENDED:
|
|
206
|
+
clearInterval(state.progressInterval);
|
|
207
|
+
window.cdpTrack?.track('video_complete', {
|
|
208
|
+
content_name: videoTitle,
|
|
209
|
+
video_duration: Math.round(player.getDuration() || 0),
|
|
210
|
+
video_provider: 'youtube'
|
|
211
|
+
});
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
})();
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
O listener dispara via `cdpTrack.track()`:
|
|
219
|
+
- `video_start` — primeiros 2s de play
|
|
220
|
+
- `video_25`, `video_50`, `video_75` — marcos de progresso (25/50/75%)
|
|
221
|
+
- `video_complete` — 100% assistido
|
|
109
222
|
|
|
110
223
|
### 3. Evento de Lead após assistir vídeo (imóveis)
|
|
111
224
|
|
|
112
225
|
```javascript
|
|
113
226
|
// Disparar Lead qualificado quando usuário assiste 75%+ do tour
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
}
|
|
227
|
+
// Adicionar dentro do callback de milestoneEvents no IFrame API listener:
|
|
228
|
+
// milestoneEvents[75] → 'video_75' — adicionar lógica abaixo no bloco forEach
|
|
229
|
+
if (milestone === 75) {
|
|
230
|
+
window.cdpTrack?.track('InitiateCheckout', {
|
|
231
|
+
content_name: 'Tour_Virtual_Empreendimento',
|
|
232
|
+
value: 0,
|
|
233
|
+
currency: 'BRL',
|
|
234
|
+
// Sinaliza alta intenção para Meta + Google
|
|
235
|
+
meta_intensity: 'high',
|
|
236
|
+
});
|
|
237
|
+
}
|
|
125
238
|
```
|
|
126
239
|
|
|
127
240
|
### 4. Consent Mode v2 — OBRIGATÓRIO para YouTube/Google Ads
|
|
@@ -161,9 +274,11 @@ Para verificar persistência correta:
|
|
|
161
274
|
```javascript
|
|
162
275
|
// No sendGA4Mp() — adicionar mapeamento de eventos YouTube
|
|
163
276
|
const VIDEO_GA4_MAP = {
|
|
164
|
-
video_start:
|
|
165
|
-
|
|
166
|
-
|
|
277
|
+
video_start: 'video_start',
|
|
278
|
+
video_25: 'video_progress', // GA4 usa video_progress com percent
|
|
279
|
+
video_50: 'video_progress',
|
|
280
|
+
video_75: 'video_progress',
|
|
281
|
+
video_complete: 'video_complete',
|
|
167
282
|
};
|
|
168
283
|
|
|
169
284
|
// Params obrigatórios para video_progress (GA4)
|
|
@@ -228,9 +343,9 @@ if (!payload.gclid && payload.utmSource === 'youtube') {
|
|
|
228
343
|
| Evento cdpTrack | Mapeamento GA4 | Mapeamento Google Ads | Quando Disparar |
|
|
229
344
|
|---|---|---|---|
|
|
230
345
|
| `video_start` | `video_start` | — | Primeiros 2s de reprodução |
|
|
231
|
-
| `
|
|
232
|
-
| `
|
|
233
|
-
| `
|
|
346
|
+
| `video_25` | `video_progress` | — | 25% assistido |
|
|
347
|
+
| `video_50` | `video_progress` | `engaged_view` candidate | 50% assistido |
|
|
348
|
+
| `video_75` | `video_progress` | `engaged_view` | 75% assistido — alta intenção |
|
|
234
349
|
| `video_complete` | `video_complete` | `video_view_complete` | 100% assistido |
|
|
235
350
|
| `Lead` (após vídeo) | `generate_lead` | Conversão primária | Formulário submetido |
|
|
236
351
|
| `InitiateCheckout` | `begin_checkout` | Conversão micro | Clique em "Quero saber mais" |
|
|
@@ -244,7 +359,7 @@ if (!payload.gclid && payload.utmSource === 'youtube') {
|
|
|
244
359
|
```
|
|
245
360
|
FASE 1: AWARENESS (YouTube TrueView 30s)
|
|
246
361
|
↓ Tour aéreo do empreendimento / lifestyle do bairro
|
|
247
|
-
↓ Rastrear:
|
|
362
|
+
↓ Rastrear: video_50 + video_75 → score alto no LTV
|
|
248
363
|
↓ Remarketing: quem assistiu 50%+ vira audiência no Google Ads
|
|
249
364
|
|
|
250
365
|
FASE 2: CONSIDERAÇÃO (YouTube Non-skip 15s + Display)
|
|
@@ -212,10 +212,14 @@
|
|
|
212
212
|
]
|
|
213
213
|
},
|
|
214
214
|
"conversions_api": {
|
|
215
|
-
"current": "
|
|
216
|
-
"minimum_supported": "
|
|
217
|
-
"recommended": "
|
|
218
|
-
"endpoint_pattern": "https://api.linkedin.com/rest/
|
|
215
|
+
"current": "202401",
|
|
216
|
+
"minimum_supported": "202401",
|
|
217
|
+
"recommended": "202401",
|
|
218
|
+
"endpoint_pattern": "https://api.linkedin.com/rest/conversionEvents",
|
|
219
|
+
"required_headers": {
|
|
220
|
+
"LinkedIn-Version": "202401",
|
|
221
|
+
"X-Restli-Protocol-Version": "2.0.0"
|
|
222
|
+
},
|
|
219
223
|
"authentication": "Bearer token (LINKEDIN_ACCESS_TOKEN)",
|
|
220
224
|
"rate_limits": {
|
|
221
225
|
"requests_per_second": 10,
|
|
@@ -359,10 +363,10 @@
|
|
|
359
363
|
},
|
|
360
364
|
|
|
361
365
|
"last_updated_by": {
|
|
362
|
-
"agent": "
|
|
363
|
-
"session_id": "CDP_2026-
|
|
364
|
-
"timestamp": "2026-
|
|
366
|
+
"agent": "Audit — CDP Edge v2.0",
|
|
367
|
+
"session_id": "CDP_2026-04-10_audit",
|
|
368
|
+
"timestamp": "2026-04-10T00:00:00.000Z"
|
|
365
369
|
},
|
|
366
370
|
|
|
367
|
-
"next_review_date": "2026-
|
|
371
|
+
"next_review_date": "2026-05-10T00:00:00.000Z"
|
|
368
372
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cdp-edge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.1",
|
|
4
4
|
"description": "CDP Edge - Quantum Tracking - Sistema multi-agente para tracking digital Cloudflare Native (Workers + D1)",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"build": "node build.js",
|
|
24
24
|
"dev": "node build.js --watch",
|
|
25
25
|
"test": "node test.js",
|
|
26
|
-
"test:unit": "node tests/unit
|
|
26
|
+
"test:unit": "node tests/unit/normalization.test.js && node tests/unit/hashing.test.js && node tests/unit/deduplication.test.js && node tests/unit/payload-validation.test.js && node tests/unit/new-features.test.js",
|
|
27
27
|
"test:unit:normalize": "node tests/unit/normalization.test.js",
|
|
28
28
|
"test:unit:hash": "node tests/unit/hashing.test.js",
|
|
29
29
|
"test:unit:dedup": "node tests/unit/deduplication.test.js",
|
|
@@ -151,7 +151,7 @@ async function sendMetaCapi(env, eventName, payload, request, ctx) {
|
|
|
151
151
|
if (!res.ok) {
|
|
152
152
|
const errorCode = data.error?.code || String(res.status);
|
|
153
153
|
const errorMessage = data.error?.message || data.error?.error_user_msg || 'Unknown error';
|
|
154
|
-
console.error('Meta CAPI error:',
|
|
154
|
+
console.error('Meta CAPI error:', res.status, data.error?.message || data.error?.error_user_msg || 'unknown');
|
|
155
155
|
|
|
156
156
|
// Log de falha para Feedback Loop
|
|
157
157
|
if (env.DB) {
|
|
@@ -802,7 +802,7 @@ async function sendTikTokApi(env, eventName, payload, request, ctx) {
|
|
|
802
802
|
|
|
803
803
|
const data = await res.json();
|
|
804
804
|
if (!res.ok || data.code !== 0) {
|
|
805
|
-
console.error('TikTok Events API error:',
|
|
805
|
+
console.error('TikTok Events API error:', res.status, data.message || data.code || 'unknown');
|
|
806
806
|
|
|
807
807
|
// Log de falha para Feedback Loop
|
|
808
808
|
if (env.DB && ctx) {
|
|
@@ -907,8 +907,9 @@ async function sendPinterestCapi(env, eventName, payload, request, ctx) {
|
|
|
907
907
|
);
|
|
908
908
|
const data = await res.json();
|
|
909
909
|
if (!res.ok) {
|
|
910
|
-
|
|
911
|
-
|
|
910
|
+
const pinterestErrMsg = data.message || data.code || String(res.status);
|
|
911
|
+
console.error('Pinterest CAPI error:', res.status, pinterestErrMsg);
|
|
912
|
+
if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'pinterest', eventName, String(res.status), pinterestErrMsg, body.data[0].event_id, JSON.stringify(body)));
|
|
912
913
|
}
|
|
913
914
|
return data;
|
|
914
915
|
} catch (err) {
|
|
@@ -1306,7 +1307,7 @@ async function _sendWARequest(env, body) {
|
|
|
1306
1307
|
body: JSON.stringify(body),
|
|
1307
1308
|
});
|
|
1308
1309
|
const data = await res.json();
|
|
1309
|
-
if (!res.ok) console.error('WhatsApp Meta API error:',
|
|
1310
|
+
if (!res.ok) console.error('WhatsApp Meta API error:', res.status, data.error?.message || 'unknown');
|
|
1310
1311
|
return { ok: res.ok, status: res.status, data };
|
|
1311
1312
|
} catch (err) {
|
|
1312
1313
|
console.error('WhatsApp Meta API failed:', err.message);
|
|
@@ -1452,7 +1453,7 @@ async function processWhatsAppWebhook(env, body, request, ctx) {
|
|
|
1452
1453
|
'UPDATE whatsapp_contacts SET capi_sent = 1 WHERE wamid = ?'
|
|
1453
1454
|
).bind(wamid).run();
|
|
1454
1455
|
} else if (!res.ok) {
|
|
1455
|
-
console.error('[CTWA] Meta CAPI error:',
|
|
1456
|
+
console.error('[CTWA] Meta CAPI error:', res.status, data.error?.message || 'unknown');
|
|
1456
1457
|
if (env.DB) {
|
|
1457
1458
|
await logApiFailure(env.DB, 'meta', 'Contact', data.error?.code || res.status,
|
|
1458
1459
|
data.error?.message || 'CTWA CAPI error', eventId, JSON.stringify(requestBody));
|
|
@@ -1931,7 +1932,7 @@ async function runIntelligenceAgent(env, runType) {
|
|
|
1931
1932
|
|
|
1932
1933
|
// 5. Customer Match — sync semanal D1 → Meta Custom Audience
|
|
1933
1934
|
const cmResult = await syncMetaCustomAudience(env);
|
|
1934
|
-
console.log(`[Intelligence Agent] Customer Match Meta:
|
|
1935
|
+
console.log(`[Intelligence Agent] Customer Match Meta: sent=${cmResult?.sent ?? 0}, received=${cmResult?.num_received ?? 0}`);
|
|
1935
1936
|
|
|
1936
1937
|
console.log(`[Intelligence Agent] ${runType} concluído`);
|
|
1937
1938
|
}
|
|
@@ -2000,7 +2001,7 @@ async function syncMetaCustomAudience(env) {
|
|
|
2000
2001
|
const result = await res.json();
|
|
2001
2002
|
|
|
2002
2003
|
if (!res.ok) {
|
|
2003
|
-
console.error('[CustomerMatch] Meta erro:',
|
|
2004
|
+
console.error('[CustomerMatch] Meta erro:', res.status, result.error?.message || 'unknown');
|
|
2004
2005
|
return { error: result.error?.message, sent: 0 };
|
|
2005
2006
|
}
|
|
2006
2007
|
|
|
@@ -3508,6 +3509,12 @@ export default {
|
|
|
3508
3509
|
|
|
3509
3510
|
// ── POST /track — evento do browser ───────────────────────────────────────
|
|
3510
3511
|
if (request.method === 'POST' && url.pathname === '/track') {
|
|
3512
|
+
// Reject oversized payloads before reading body (64 KB limit)
|
|
3513
|
+
const contentLength = parseInt(request.headers.get('Content-Length') || '0', 10);
|
|
3514
|
+
if (contentLength > 65536) {
|
|
3515
|
+
return new Response(JSON.stringify({ error: 'Payload muito grande' }), { status: 413, headers });
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3511
3518
|
let body;
|
|
3512
3519
|
try {
|
|
3513
3520
|
body = await request.json();
|
|
@@ -3518,6 +3525,22 @@ export default {
|
|
|
3518
3525
|
);
|
|
3519
3526
|
}
|
|
3520
3527
|
|
|
3528
|
+
// ── Payload validation ────────────────────────────────────────────────────
|
|
3529
|
+
// Reject non-object bodies and oversized string fields to prevent injection
|
|
3530
|
+
if (typeof body !== 'object' || Array.isArray(body) || body === null) {
|
|
3531
|
+
return new Response(JSON.stringify({ error: 'Payload inválido' }), { status: 400, headers });
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
const VALID_EVENT_NAMES = new Set([
|
|
3535
|
+
'PageView','ViewContent','Lead','Purchase','InitiateCheckout',
|
|
3536
|
+
'AddToCart','CompleteRegistration','Contact','Schedule',
|
|
3537
|
+
'StartTrial','Subscribe','SubmitApplication','Search',
|
|
3538
|
+
'video_start','video_25','video_50','video_75','video_complete'
|
|
3539
|
+
]);
|
|
3540
|
+
const STR_FIELDS = ['email','phone','firstName','lastName','city','state','zip','userId',
|
|
3541
|
+
'utmSource','utmMedium','utmCampaign','utmContent','utmTerm',
|
|
3542
|
+
'fbclid','ttclid','gclid','transactionId','productName','currency'];
|
|
3543
|
+
|
|
3521
3544
|
const { eventName, behavioral_data, ...payload } = body;
|
|
3522
3545
|
|
|
3523
3546
|
if (!eventName) {
|
|
@@ -3527,6 +3550,28 @@ export default {
|
|
|
3527
3550
|
);
|
|
3528
3551
|
}
|
|
3529
3552
|
|
|
3553
|
+
if (typeof eventName !== 'string' || eventName.length > 64 || !VALID_EVENT_NAMES.has(eventName)) {
|
|
3554
|
+
return new Response(JSON.stringify({ error: `eventName desconhecido: ${eventName.slice(0, 64)}` }), { status: 400, headers });
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
// Enforce max string length on known PII/UTM fields to block injection payloads
|
|
3558
|
+
for (const field of STR_FIELDS) {
|
|
3559
|
+
if (payload[field] !== undefined && payload[field] !== null) {
|
|
3560
|
+
if (typeof payload[field] !== 'string' || payload[field].length > 512) {
|
|
3561
|
+
return new Response(JSON.stringify({ error: `Campo inválido: ${field}` }), { status: 400, headers });
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
// value must be a non-negative number when present
|
|
3567
|
+
if (payload.value !== undefined && payload.value !== null) {
|
|
3568
|
+
const v = Number(payload.value);
|
|
3569
|
+
if (isNaN(v) || v < 0 || v > 9_999_999) {
|
|
3570
|
+
return new Response(JSON.stringify({ error: 'value fora do intervalo permitido' }), { status: 400, headers });
|
|
3571
|
+
}
|
|
3572
|
+
payload.value = v;
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3530
3575
|
// ── Extrair dados comportamentais do browser ──────────────────────────────
|
|
3531
3576
|
// behavioral_data vem do engagement-scoring.js (engagement_score 0-5, intention_level)
|
|
3532
3577
|
// e do BehaviorEngine (user_score 0-100)
|