cdp-edge 1.23.2 → 1.24.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 +82 -21
- package/bin/cdp-edge.js +10 -1
- package/contracts/agent-versions.json +42 -41
- package/contracts/types.ts +81 -0
- package/dist/commands/install.js +6 -1
- package/dist/commands/server.js +4 -4
- package/docs/whatsapp-ctwa.md +3 -2
- package/extracted-skill/tracking-events-generator/agents/database-agent.md +5 -4
- package/extracted-skill/tracking-events-generator/agents/fraud-detection-agent.md +0 -1
- package/extracted-skill/tracking-events-generator/agents/linkedin-agent.md +1 -1
- package/extracted-skill/tracking-events-generator/agents/ltv-predictor-agent.md +4 -4
- package/extracted-skill/tracking-events-generator/agents/ml-clustering-agent.md +81 -70
- package/extracted-skill/tracking-events-generator/agents/page-analyzer.md +6 -2
- package/extracted-skill/tracking-events-generator/cdpTrack.js +7 -0
- package/extracted-skill/tracking-events-generator/models/lancamento-imobiliario.md +344 -0
- package/extracted-skill/tracking-events-generator/route-intent-capture.js +222 -0
- package/package.json +9 -5
- package/server-edge-tracker/INSTALAR.md +5 -5
- package/server-edge-tracker/{index.js → index.ts} +186 -72
- package/server-edge-tracker/modules/{db.js → db.ts} +180 -69
- package/server-edge-tracker/modules/dispatch/{ga4.js → ga4.ts} +12 -10
- package/server-edge-tracker/modules/dispatch/meta.ts +138 -0
- package/server-edge-tracker/modules/dispatch/{platforms.js → platforms.ts} +58 -56
- package/server-edge-tracker/modules/dispatch/{tiktok.js → tiktok.ts} +22 -20
- package/server-edge-tracker/modules/dispatch/{whatsapp.js → whatsapp.ts} +59 -25
- package/server-edge-tracker/modules/{intelligence.js → intelligence.ts} +175 -60
- package/server-edge-tracker/modules/ml/{bidding.js → bidding.ts} +37 -35
- package/server-edge-tracker/modules/ml/{fraud.js → fraud.ts} +49 -56
- package/server-edge-tracker/modules/ml/{logistic.js → logistic.ts} +44 -19
- package/server-edge-tracker/modules/ml/{ltv.js → ltv.ts} +179 -83
- package/server-edge-tracker/modules/ml/{matchquality.js → matchquality.ts} +70 -26
- package/server-edge-tracker/modules/ml/segmentation.ts +407 -0
- package/server-edge-tracker/modules/utils.ts +186 -0
- package/server-edge-tracker/schema-ltv-feedback.sql +11 -0
- package/server-edge-tracker/types.ts +251 -0
- package/server-edge-tracker/wrangler.toml +24 -6
- package/templates/lancamento-imobiliario.md +344 -0
- package/docs/PixelBuilder-Documentacao-Completa (2).docx +0 -0
- package/server-edge-tracker/modules/dispatch/meta.js +0 -119
- package/server-edge-tracker/modules/ml/segmentation.js +0 -316
- package/server-edge-tracker/modules/utils.js +0 -89
- package/server-edge-tracker/worker.js +0 -4577
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CDP Edge — index.
|
|
2
|
+
* CDP Edge — index.ts (ES Module Entry Point)
|
|
3
3
|
*
|
|
4
4
|
* Este arquivo é o novo entry point modular do Worker.
|
|
5
5
|
* Para usá-lo, altere em wrangler.toml:
|
|
6
|
-
* main = "worker.js" → main = "index.
|
|
6
|
+
* main = "worker.js" → main = "index.ts"
|
|
7
7
|
*
|
|
8
8
|
* O worker.js original permanece intacto como fallback.
|
|
9
9
|
* Todos os módulos ficam em ./modules/
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { ExecutionContext } from '@cloudflare/workers-types';
|
|
13
|
+
import { Env, TrackPayload, BehavioralData, HotmartWebhook, KiwifyWebhook, TictoWebhook } from './types';
|
|
14
|
+
|
|
12
15
|
// ── Utilitários base ──────────────────────────────────────────────────────────
|
|
13
16
|
import {
|
|
14
|
-
isAllowedOrigin,
|
|
15
17
|
corsHeaders,
|
|
16
18
|
sha256,
|
|
17
19
|
META_TO_GA4,
|
|
18
20
|
VALID_EVENT_NAMES,
|
|
19
|
-
|
|
21
|
+
resolveFunnelStage,
|
|
22
|
+
resolveIntentScore,
|
|
23
|
+
distanceBucketWeight,
|
|
24
|
+
computeMetaSignalWeights,
|
|
25
|
+
metaSignalBucket,
|
|
26
|
+
} from './modules/utils';
|
|
20
27
|
|
|
21
28
|
// ── Banco de dados (D1) ───────────────────────────────────────────────────────
|
|
22
29
|
import {
|
|
@@ -31,23 +38,24 @@ import {
|
|
|
31
38
|
saveEdgeFingerprint,
|
|
32
39
|
resurrectUTM,
|
|
33
40
|
upsertLtvProfile,
|
|
34
|
-
|
|
41
|
+
recordLtvFeedback,
|
|
42
|
+
} from './modules/db';
|
|
35
43
|
|
|
36
44
|
// ── Dispatch — plataformas de ads ─────────────────────────────────────────────
|
|
37
|
-
import { sendMetaCapi } from './modules/dispatch/meta
|
|
38
|
-
import { sendGA4Mp } from './modules/dispatch/ga4
|
|
39
|
-
import { sendTikTokApi } from './modules/dispatch/tiktok
|
|
45
|
+
import { sendMetaCapi } from './modules/dispatch/meta';
|
|
46
|
+
import { sendGA4Mp } from './modules/dispatch/ga4';
|
|
47
|
+
import { sendTikTokApi } from './modules/dispatch/tiktok';
|
|
40
48
|
import {
|
|
41
49
|
sendPinterestCapi,
|
|
42
50
|
sendRedditCapi,
|
|
43
51
|
sendLinkedInCapi,
|
|
44
52
|
sendSpotifyCapi,
|
|
45
|
-
} from './modules/dispatch/platforms
|
|
53
|
+
} from './modules/dispatch/platforms';
|
|
46
54
|
import {
|
|
47
55
|
sendWhatsApp,
|
|
48
56
|
processWhatsAppWebhook,
|
|
49
57
|
verifyHmac,
|
|
50
|
-
} from './modules/dispatch/whatsapp
|
|
58
|
+
} from './modules/dispatch/whatsapp';
|
|
51
59
|
|
|
52
60
|
// ── ML — LTV + A/B Testing ────────────────────────────────────────────────────
|
|
53
61
|
import {
|
|
@@ -58,7 +66,7 @@ import {
|
|
|
58
66
|
handleLtvAbTestList,
|
|
59
67
|
handleLtvAbTestResults,
|
|
60
68
|
handleLtvAbTestWinner,
|
|
61
|
-
} from './modules/ml/ltv
|
|
69
|
+
} from './modules/ml/ltv';
|
|
62
70
|
|
|
63
71
|
// ── ML — Segmentação ──────────────────────────────────────────────────────────
|
|
64
72
|
import {
|
|
@@ -66,14 +74,14 @@ import {
|
|
|
66
74
|
handleSegmentationList,
|
|
67
75
|
handleSegmentationOutliers,
|
|
68
76
|
handleSegmentationUpdate,
|
|
69
|
-
} from './modules/ml/segmentation
|
|
77
|
+
} from './modules/ml/segmentation';
|
|
70
78
|
|
|
71
79
|
// ── ML — Bidding ──────────────────────────────────────────────────────────────
|
|
72
80
|
import {
|
|
73
81
|
handleBiddingRecommend,
|
|
74
82
|
handleBiddingHistory,
|
|
75
83
|
handleBiddingStatus,
|
|
76
|
-
} from './modules/ml/bidding
|
|
84
|
+
} from './modules/ml/bidding';
|
|
77
85
|
|
|
78
86
|
// ── ML — Fraud Detection ──────────────────────────────────────────────────────
|
|
79
87
|
import {
|
|
@@ -84,25 +92,40 @@ import {
|
|
|
84
92
|
handleFraudBlocklistAdd,
|
|
85
93
|
handleFraudBlocklistRemove,
|
|
86
94
|
handleFraudStats,
|
|
87
|
-
} from './modules/ml/fraud
|
|
95
|
+
} from './modules/ml/fraud';
|
|
88
96
|
|
|
89
97
|
// ── Intelligence Agent (Cron) ─────────────────────────────────────────────────
|
|
90
98
|
import {
|
|
91
99
|
runIntelligenceAgent,
|
|
92
100
|
buildGoogleCustomerMatchExport,
|
|
93
|
-
} from './modules/intelligence
|
|
101
|
+
} from './modules/intelligence';
|
|
102
|
+
|
|
103
|
+
// ── Haversine distance (km) — sem dependência externa ────────────────────────
|
|
104
|
+
function haversineKm(lat1: number | string | null | undefined, lon1: number | string | null | undefined, lat2: number | string | null | undefined, lon2: number | string | null | undefined): number {
|
|
105
|
+
const R = 6371;
|
|
106
|
+
const lat1Num = parseFloat(String(lat1 ?? '0'));
|
|
107
|
+
const lon1Num = parseFloat(String(lon1 ?? '0'));
|
|
108
|
+
const lat2Num = parseFloat(String(lat2 ?? '0'));
|
|
109
|
+
const lon2Num = parseFloat(String(lon2 ?? '0'));
|
|
110
|
+
const dLat = (lat2Num - lat1Num) * Math.PI / 180;
|
|
111
|
+
const dLon = (lon2Num - lon1Num) * Math.PI / 180;
|
|
112
|
+
const a = Math.sin(dLat / 2) ** 2 +
|
|
113
|
+
Math.cos(lat1Num * Math.PI / 180) * Math.cos(lat2Num * Math.PI / 180) * Math.sin(dLon / 2) ** 2;
|
|
114
|
+
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
115
|
+
}
|
|
94
116
|
|
|
95
117
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
96
118
|
// HANDLER PRINCIPAL
|
|
97
119
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
98
120
|
export default {
|
|
99
121
|
|
|
100
|
-
async fetch(request, env, ctx) {
|
|
122
|
+
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
|
101
123
|
const origin = request.headers.get('Origin') || '';
|
|
102
|
-
const
|
|
124
|
+
const headersObj = {
|
|
103
125
|
'Content-Type': 'application/json',
|
|
104
|
-
...corsHeaders(origin, env.SITE_DOMAIN),
|
|
126
|
+
...corsHeaders(origin, env.SITE_DOMAIN || null),
|
|
105
127
|
};
|
|
128
|
+
const headers = new Headers(headersObj);
|
|
106
129
|
|
|
107
130
|
// Preflight CORS
|
|
108
131
|
if (request.method === 'OPTIONS') {
|
|
@@ -126,10 +149,10 @@ export default {
|
|
|
126
149
|
// Roda ANTES de qualquer processamento de evento
|
|
127
150
|
// Silent drop (200) — bots não sabem que foram detectados
|
|
128
151
|
if (url.pathname === '/track' && request.method === 'POST') {
|
|
129
|
-
let trackBodyForFraud;
|
|
152
|
+
let trackBodyForFraud: TrackPayload = {};
|
|
130
153
|
try {
|
|
131
154
|
const cloned = request.clone();
|
|
132
|
-
trackBodyForFraud = await cloned.json().catch(() => ({}));
|
|
155
|
+
trackBodyForFraud = await cloned.json().catch(() => ({})) as TrackPayload;
|
|
133
156
|
} catch { trackBodyForFraud = {}; }
|
|
134
157
|
|
|
135
158
|
const fraudResult = await checkFraudGate(env, request, trackBodyForFraud);
|
|
@@ -158,30 +181,30 @@ export default {
|
|
|
158
181
|
|
|
159
182
|
// ── GET /health ───────────────────────────────────────────────────────────
|
|
160
183
|
if (request.method === 'GET' && url.pathname === '/health') {
|
|
161
|
-
const results = {};
|
|
184
|
+
const results: Record<string, string> = {};
|
|
162
185
|
|
|
163
186
|
try {
|
|
164
|
-
await env.DB
|
|
187
|
+
await env.DB?.prepare('SELECT 1').run();
|
|
165
188
|
results.d1 = 'ok';
|
|
166
|
-
} catch (err) {
|
|
167
|
-
results.d1 = `FAILED: ${err
|
|
189
|
+
} catch (err: any) {
|
|
190
|
+
results.d1 = `FAILED: ${err?.message || String(err)}`;
|
|
168
191
|
}
|
|
169
192
|
|
|
170
193
|
try {
|
|
171
|
-
await env.GEO_CACHE
|
|
194
|
+
await env.GEO_CACHE?.get('__health_check__');
|
|
172
195
|
results.kv = 'ok';
|
|
173
|
-
} catch (err) {
|
|
174
|
-
results.kv = `FAILED: ${err
|
|
196
|
+
} catch (err: any) {
|
|
197
|
+
results.kv = `FAILED: ${err?.message || String(err)}`;
|
|
175
198
|
}
|
|
176
199
|
|
|
177
200
|
try {
|
|
178
|
-
await env.AI
|
|
201
|
+
await env.AI?.run('@cf/ibm-granite/granite-4.0-h-micro', {
|
|
179
202
|
messages: [{ role: 'user', content: 'ping' }],
|
|
180
203
|
max_tokens: 1,
|
|
181
204
|
});
|
|
182
205
|
results.ai = 'ok';
|
|
183
|
-
} catch (err) {
|
|
184
|
-
results.ai = `FAILED: ${err
|
|
206
|
+
} catch (err: any) {
|
|
207
|
+
results.ai = `FAILED: ${err?.message || String(err)}`;
|
|
185
208
|
}
|
|
186
209
|
|
|
187
210
|
const vars = {
|
|
@@ -239,7 +262,8 @@ export default {
|
|
|
239
262
|
'utmSource','utmMedium','utmCampaign','utmContent','utmTerm',
|
|
240
263
|
'fbclid','ttclid','gclid','transactionId','productName','currency'];
|
|
241
264
|
|
|
242
|
-
const { eventName, behavioral_data, ...payload } = body;
|
|
265
|
+
const { eventName, behavioral_data, ...payload } = body as { eventName?: string; behavioral_data?: BehavioralData; [key: string]: any };
|
|
266
|
+
const trackPayload: TrackPayload = payload;
|
|
243
267
|
|
|
244
268
|
if (!eventName) {
|
|
245
269
|
return new Response(JSON.stringify({ error: 'eventName é obrigatório' }), { status: 400, headers });
|
|
@@ -250,19 +274,19 @@ export default {
|
|
|
250
274
|
}
|
|
251
275
|
|
|
252
276
|
for (const field of STR_FIELDS) {
|
|
253
|
-
if (
|
|
254
|
-
if (typeof
|
|
277
|
+
if (trackPayload[field as keyof TrackPayload] !== undefined && trackPayload[field as keyof TrackPayload] !== null) {
|
|
278
|
+
if (typeof trackPayload[field as keyof TrackPayload] !== 'string' || String(trackPayload[field as keyof TrackPayload]).length > 512) {
|
|
255
279
|
return new Response(JSON.stringify({ error: `Campo inválido: ${field}` }), { status: 400, headers });
|
|
256
280
|
}
|
|
257
281
|
}
|
|
258
282
|
}
|
|
259
283
|
|
|
260
|
-
if (
|
|
261
|
-
const v = Number(
|
|
284
|
+
if (trackPayload.value !== undefined && trackPayload.value !== null) {
|
|
285
|
+
const v = Number(trackPayload.value);
|
|
262
286
|
if (isNaN(v) || v < 0 || v > 9_999_999) {
|
|
263
287
|
return new Response(JSON.stringify({ error: 'value fora do intervalo permitido' }), { status: 400, headers });
|
|
264
288
|
}
|
|
265
|
-
|
|
289
|
+
trackPayload.value = v;
|
|
266
290
|
}
|
|
267
291
|
|
|
268
292
|
// ── Extrair dados comportamentais do browser ──────────────────────────
|
|
@@ -270,6 +294,9 @@ export default {
|
|
|
270
294
|
payload.engagementScore = behavioral_data.engagement_score ?? behavioral_data.totalScore ?? null;
|
|
271
295
|
payload.intentionLevel = behavioral_data.intention_level ?? null;
|
|
272
296
|
payload.userScore = behavioral_data.user_score ?? null;
|
|
297
|
+
// Sinais de engajamento profundo — usados no anti-falso-positivo e no LTV
|
|
298
|
+
payload.scrollScore = behavioral_data.scroll_score ?? null;
|
|
299
|
+
payload.timeLevel = behavioral_data.time_level ?? null;
|
|
273
300
|
payload.email = payload.email || behavioral_data.email || null;
|
|
274
301
|
payload.phone = payload.phone || behavioral_data.phone || null;
|
|
275
302
|
payload.firstName = payload.firstName || behavioral_data.first_name || behavioral_data.firstName || null;
|
|
@@ -280,11 +307,40 @@ export default {
|
|
|
280
307
|
payload.dob = payload.dob || behavioral_data.dob || null;
|
|
281
308
|
}
|
|
282
309
|
|
|
310
|
+
// ── Normalização de intent_score → 0.0–1.0 ──────────────────────────
|
|
311
|
+
// Aceita string ('high'/'medium'/'low'), 0-1 ou 0-100 enviados pelo front.
|
|
312
|
+
// intent_bucket mantém a label legível para D1 e logs.
|
|
313
|
+
const intentScoreNum = resolveIntentScore(payload.intent_score);
|
|
314
|
+
if (intentScoreNum !== null) {
|
|
315
|
+
payload.intent_score = intentScoreNum;
|
|
316
|
+
payload.intentScoreNum = intentScoreNum;
|
|
317
|
+
payload.intent_bucket = intentScoreNum >= 0.8 ? 'high'
|
|
318
|
+
: intentScoreNum >= 0.5 ? 'medium' : 'low';
|
|
319
|
+
} else {
|
|
320
|
+
payload.intentScoreNum = null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── Anti-falso-positivo ───────────────────────────────────────────────
|
|
324
|
+
// Penaliza intent se engajamento insuficiente: scroll raso E tempo curto.
|
|
325
|
+
// scroll_score < 2.0 ≈ não passou de 50% da página.
|
|
326
|
+
// time_level 'curioso' = menos de 60 segundos na página.
|
|
327
|
+
if (payload.intentScoreNum !== null) {
|
|
328
|
+
const isShallowScroll = payload.scrollScore !== null && payload.scrollScore < 2.0;
|
|
329
|
+
const isShallowTime = payload.timeLevel === 'curioso';
|
|
330
|
+
if (isShallowScroll && isShallowTime) {
|
|
331
|
+
const penalized = Math.round(payload.intentScoreNum * 0.7 * 100) / 100;
|
|
332
|
+
payload.intentScoreNum = penalized;
|
|
333
|
+
payload.intent_score = penalized;
|
|
334
|
+
payload.intent_bucket = penalized >= 0.8 ? 'high' : penalized >= 0.5 ? 'medium' : 'low';
|
|
335
|
+
payload.intent_penalized = true; // flag auditável — visível no D1 e logs
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
283
339
|
// ── Edge Fingerprint + UTM Resurrection ──────────────────────────────
|
|
284
340
|
const fingerprint = await generateEdgeFingerprint(request);
|
|
285
341
|
payload.utmRestored = false;
|
|
286
342
|
|
|
287
|
-
if (fingerprint) {
|
|
343
|
+
if (fingerprint && env.DB) {
|
|
288
344
|
if (payload.utmSource) {
|
|
289
345
|
ctx.waitUntil(saveEdgeFingerprint(env.DB, fingerprint, payload.userId, payload));
|
|
290
346
|
} else {
|
|
@@ -301,13 +357,13 @@ export default {
|
|
|
301
357
|
}
|
|
302
358
|
|
|
303
359
|
// ── Bot Mitigation ────────────────────────────────────────────────────
|
|
304
|
-
const botScoreStr = request.cf?.botManagement?.score;
|
|
305
|
-
const cfBotScore = botScoreStr !== undefined ? parseInt(botScoreStr) : 100;
|
|
360
|
+
const botScoreStr = (request as any).cf?.botManagement?.score;
|
|
361
|
+
const cfBotScore = botScoreStr !== undefined ? parseInt(String(botScoreStr)) : 100;
|
|
306
362
|
const ua = (request.headers.get('User-Agent') || '').toLowerCase();
|
|
307
363
|
const isBotPattern = /bot|crawler|spider|lighthouse|postman|curl|inspect|headless/i.test(ua);
|
|
308
364
|
|
|
309
365
|
const isBot = cfBotScore < 30 || isBotPattern;
|
|
310
|
-
|
|
366
|
+
trackPayload.botScore = isBot ? 2 : (cfBotScore < 60 ? 1 : 0);
|
|
311
367
|
|
|
312
368
|
if (isBot && !['Contact', 'Lead', 'Purchase'].includes(eventName)) {
|
|
313
369
|
return new Response(JSON.stringify({ ok: true, skipped_due_to_bot: true }), { status: 200, headers });
|
|
@@ -324,6 +380,34 @@ export default {
|
|
|
324
380
|
|
|
325
381
|
const ga4Name = META_TO_GA4[eventName] || eventName.toLowerCase();
|
|
326
382
|
|
|
383
|
+
// ── Dual-layer semantics ─────────────────────────────────────────────
|
|
384
|
+
// Meta sempre recebe o nome canônico (Schedule, Lead, etc.).
|
|
385
|
+
// Internamente o CDP usa semântica de funil precisa via FUNNEL_TAXONOMY.
|
|
386
|
+
if (payload.funnel_stage) {
|
|
387
|
+
const { depth, funnelDepth } = resolveFunnelStage(payload.funnel_stage);
|
|
388
|
+
payload.funnelDepth = funnelDepth; // ex: 'bottom_intent', 'bottom_conversion'
|
|
389
|
+
payload.funnelLevel = depth; // ex: 'bottom', 'conversion', 'mid'
|
|
390
|
+
}
|
|
391
|
+
if (eventName === 'Schedule' && payload.funnel_stage === 'route_click') {
|
|
392
|
+
payload.internalEvent = 'IntentToVisit';
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ── Real Estate Distance Enrichment ──────────────────────────────────
|
|
396
|
+
// Calcula distância entre o IP do usuário (via Cloudflare) e o imóvel.
|
|
397
|
+
// Ativa quando o evento carrega property_lat + property_lng (ex: Schedule de rota).
|
|
398
|
+
const propLat = parseFloat(String(trackPayload.property_lat ?? trackPayload.propertyLat));
|
|
399
|
+
const propLng = parseFloat(String(trackPayload.property_lng ?? trackPayload.propertyLng));
|
|
400
|
+
const userLat = parseFloat(String(request.cf?.latitude ?? '0'));
|
|
401
|
+
const userLng = parseFloat(String(request.cf?.longitude ?? '0'));
|
|
402
|
+
if (!isNaN(propLat) && !isNaN(propLng) && !isNaN(userLat) && !isNaN(userLng)) {
|
|
403
|
+
const distKm = haversineKm(userLat, userLng, propLat, propLng);
|
|
404
|
+
trackPayload.distanceKm = Math.round(distKm * 10) / 10;
|
|
405
|
+
trackPayload.distanceBucket = distKm < 5 ? 'very_close' :
|
|
406
|
+
distKm < 15 ? 'close' :
|
|
407
|
+
distKm < 30 ? 'nearby' :
|
|
408
|
+
distKm < 60 ? 'moderate' : 'far';
|
|
409
|
+
}
|
|
410
|
+
|
|
327
411
|
// ── LTV Prediction (+ A/B Testing de Prompts) ─────────────────────────
|
|
328
412
|
const LTV_EVENTS = ['Lead', 'Contact', 'Schedule', 'CompleteRegistration'];
|
|
329
413
|
if (LTV_EVENTS.includes(eventName) && !payload.value) {
|
|
@@ -342,16 +426,45 @@ export default {
|
|
|
342
426
|
recordAbAssignment(
|
|
343
427
|
env,
|
|
344
428
|
payload.userId,
|
|
345
|
-
abVariation.
|
|
429
|
+
abVariation.id,
|
|
346
430
|
abVariation.test_id,
|
|
347
431
|
ltv.value,
|
|
348
432
|
ltv.class,
|
|
349
|
-
emailHash,
|
|
433
|
+
emailHash ?? null,
|
|
350
434
|
)
|
|
351
435
|
);
|
|
352
436
|
}
|
|
353
437
|
}
|
|
354
438
|
|
|
439
|
+
// ── LTV Feedback Loop — fecha o ciclo preditivo ──────────────────────
|
|
440
|
+
// Quando uma compra real acontece, registra o valor real e recalcula accuracy.
|
|
441
|
+
// Alimenta ltv_ab_variations.accuracy_score → autoDecideAbWinner usa isso.
|
|
442
|
+
if (eventName === 'Purchase' && payload.value > 0) {
|
|
443
|
+
ctx.waitUntil(recordLtvFeedback(env, payload.userId, payload.value));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ── Meta Signal Score (composite, pesos dinâmicos) ───────────────────
|
|
447
|
+
// Pesos variam por profundidade de funil: fundo = comportamento pesa mais.
|
|
448
|
+
{
|
|
449
|
+
const w = computeMetaSignalWeights(payload.funnelLevel);
|
|
450
|
+
const iW = payload.intentScoreNum ?? 0.5;
|
|
451
|
+
const lW = payload.ltvScore ? payload.ltvScore / 100 : 0.5;
|
|
452
|
+
const dW = distanceBucketWeight(payload.distanceBucket);
|
|
453
|
+
payload.metaSignal = Math.round(((iW * w.intent) + (lW * w.ltv) + (dW * w.dist)) * 100) / 100;
|
|
454
|
+
payload.metaSignalBucket = metaSignalBucket(payload.metaSignal); // 'hot'/'warm'/'cold'
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ── Hot Lead Trigger (timing + sinal) ────────────────────────────────
|
|
458
|
+
// Reage em tempo real: WhatsApp apenas quando comportamento + sinal justificam.
|
|
459
|
+
// Critérios: rota + muito próximo + (intent alto OU meta_signal alto)
|
|
460
|
+
// + (janela de decisão BRT 18–22h OU sinal excepcional ≥ 0.9)
|
|
461
|
+
const hourBRT = (new Date().getUTCHours() - 3 + 24) % 24;
|
|
462
|
+
const inWindow = hourBRT >= 18 && hourBRT <= 22;
|
|
463
|
+
const isHotLead = payload.funnel_stage === 'route_click'
|
|
464
|
+
&& payload.distanceBucket === 'very_close'
|
|
465
|
+
&& ((payload.intentScoreNum ?? 0) >= 0.8 || payload.metaSignal >= 0.85)
|
|
466
|
+
&& (inWindow || payload.metaSignal >= 0.9);
|
|
467
|
+
|
|
355
468
|
// Cross-Device Graph — background
|
|
356
469
|
if (env.DB && payload.userId && (payload.email || payload.phone)) {
|
|
357
470
|
ctx.waitUntil(resolveDeviceGraph(env.DB, payload.userId, payload.email, payload.phone));
|
|
@@ -366,36 +479,37 @@ export default {
|
|
|
366
479
|
sendMetaCapi(env, eventName, payload, request, ctx),
|
|
367
480
|
sendGA4Mp(env, ga4Name, payload, ctx),
|
|
368
481
|
sendTikTokApi(env, eventName, payload, request, ctx),
|
|
369
|
-
saveLead(env, eventName, payload, request, 'website'),
|
|
482
|
+
saveLead(env, payload.internalEvent || eventName, payload, request, 'website'),
|
|
370
483
|
upsertProfile(env, eventName, payload, request),
|
|
371
|
-
...(WHATSAPP_NOTIFY_EVENTS.includes(eventName)
|
|
372
|
-
? [sendWhatsApp(env, eventName, payload)]
|
|
484
|
+
...(WHATSAPP_NOTIFY_EVENTS.includes(eventName) || isHotLead
|
|
485
|
+
? [sendWhatsApp(env, isHotLead ? 'hot_lead_intent_to_visit' : eventName, payload)]
|
|
373
486
|
: []),
|
|
374
487
|
]);
|
|
375
488
|
|
|
376
489
|
// Automação de mensagens
|
|
377
490
|
const AUTOMATION_EVENTS = ['Lead', 'Purchase', 'InitiateCheckout'];
|
|
378
491
|
if (AUTOMATION_EVENTS.includes(eventName) && env.DB) {
|
|
492
|
+
const db = env.DB; // Captura em variável local
|
|
379
493
|
ctx.waitUntil(
|
|
380
494
|
(async () => {
|
|
381
495
|
try {
|
|
382
|
-
const lastLead = await
|
|
496
|
+
const lastLead = await db
|
|
383
497
|
.prepare(`SELECT id FROM leads WHERE event_id = ?1 LIMIT 1`)
|
|
384
|
-
.bind(
|
|
385
|
-
.first();
|
|
386
|
-
const leadId = lastLead?.id
|
|
387
|
-
if (leadId) await fireAutomation(env, eventName, leadId,
|
|
388
|
-
} catch (e) { console.error('[Automation] lead lookup error:', e
|
|
498
|
+
.bind(trackPayload.eventId || trackPayload.event_id || '')
|
|
499
|
+
.first() as any;
|
|
500
|
+
const leadId = lastLead?.id ? Number(lastLead.id) : null;
|
|
501
|
+
if (leadId) await fireAutomation(env, eventName, leadId, trackPayload);
|
|
502
|
+
} catch (e: any) { console.error('[Automation] lead lookup error:', e?.message || String(e)); }
|
|
389
503
|
})()
|
|
390
504
|
);
|
|
391
505
|
}
|
|
392
506
|
|
|
393
507
|
// Edge Personalization
|
|
394
508
|
let currentScore = 0;
|
|
395
|
-
if (env.DB &&
|
|
509
|
+
if (env.DB && trackPayload.userId) {
|
|
396
510
|
try {
|
|
397
|
-
const profileRow = await env.DB.prepare('SELECT score FROM user_profiles WHERE user_id = ?').bind(
|
|
398
|
-
if (profileRow) currentScore = profileRow.score;
|
|
511
|
+
const profileRow = await env.DB.prepare('SELECT score FROM user_profiles WHERE user_id = ?').bind(trackPayload.userId).first();
|
|
512
|
+
if (profileRow) currentScore = Number(profileRow.score) || 0;
|
|
399
513
|
} catch {}
|
|
400
514
|
}
|
|
401
515
|
|
|
@@ -405,9 +519,9 @@ export default {
|
|
|
405
519
|
return new Response(JSON.stringify({
|
|
406
520
|
ok: true,
|
|
407
521
|
userProfile: { score: currentScore, user_id: finalUserId },
|
|
408
|
-
meta: metaRes.value
|
|
409
|
-
ga4: ga4Res.value
|
|
410
|
-
tiktok: ttRes.value
|
|
522
|
+
meta: metaRes.status === 'fulfilled' ? metaRes.value : { error: metaRes.reason?.message },
|
|
523
|
+
ga4: ga4Res.status === 'fulfilled' ? ga4Res.value : { error: ga4Res.reason?.message },
|
|
524
|
+
tiktok: ttRes.status === 'fulfilled' ? ttRes.value : { error: ttRes.reason?.message },
|
|
411
525
|
}), { status: 200, headers: resHeaders });
|
|
412
526
|
}
|
|
413
527
|
|
|
@@ -420,8 +534,8 @@ export default {
|
|
|
420
534
|
}
|
|
421
535
|
}
|
|
422
536
|
|
|
423
|
-
let wh;
|
|
424
|
-
try { wh = await request.json(); } catch {
|
|
537
|
+
let wh: HotmartWebhook;
|
|
538
|
+
try { wh = await request.json() as HotmartWebhook; } catch {
|
|
425
539
|
return new Response('JSON inválido', { status: 400 });
|
|
426
540
|
}
|
|
427
541
|
|
|
@@ -498,8 +612,8 @@ export default {
|
|
|
498
612
|
}
|
|
499
613
|
}
|
|
500
614
|
|
|
501
|
-
let wh;
|
|
502
|
-
try { wh = await request.json(); } catch {
|
|
615
|
+
let wh: KiwifyWebhook;
|
|
616
|
+
try { wh = await request.json() as KiwifyWebhook; } catch {
|
|
503
617
|
return new Response('JSON inválido', { status: 400 });
|
|
504
618
|
}
|
|
505
619
|
|
|
@@ -520,8 +634,8 @@ export default {
|
|
|
520
634
|
}
|
|
521
635
|
|
|
522
636
|
const customer = wh.Customer || {};
|
|
523
|
-
const product = wh.Product
|
|
524
|
-
const profile = await getProfileByEmail(env, customer.email);
|
|
637
|
+
const product = wh.Product || {};
|
|
638
|
+
const profile = await getProfileByEmail(env, customer.email || '');
|
|
525
639
|
|
|
526
640
|
const payload = {
|
|
527
641
|
email: customer.email,
|
|
@@ -582,8 +696,8 @@ export default {
|
|
|
582
696
|
}
|
|
583
697
|
}
|
|
584
698
|
|
|
585
|
-
let wh;
|
|
586
|
-
try { wh = JSON.parse(rawBody); } catch {
|
|
699
|
+
let wh: TictoWebhook;
|
|
700
|
+
try { wh = JSON.parse(rawBody) as TictoWebhook; } catch {
|
|
587
701
|
return new Response('JSON inválido', { status: 400 });
|
|
588
702
|
}
|
|
589
703
|
|
|
@@ -598,7 +712,7 @@ export default {
|
|
|
598
712
|
const tracking = wh.tracking || wh.url_params || {};
|
|
599
713
|
|
|
600
714
|
const valueRaw = order.paid_amount ?? order.total ?? order.amount;
|
|
601
|
-
const value = valueRaw ? parseFloat(valueRaw) / 100 : undefined;
|
|
715
|
+
const value = valueRaw ? parseFloat(String(valueRaw)) / 100 : undefined;
|
|
602
716
|
const transactionId = order.hash || order.transaction_hash || order.id;
|
|
603
717
|
const tcTxId = String(order.hash || order.transaction_hash || order.id || '');
|
|
604
718
|
|
|
@@ -614,7 +728,7 @@ export default {
|
|
|
614
728
|
}
|
|
615
729
|
|
|
616
730
|
const urlUserId = tracking.user_id || wh.url_params?.user_id;
|
|
617
|
-
let profile = await getProfileByEmail(env, customer.email);
|
|
731
|
+
let profile = await getProfileByEmail(env, customer.email || '');
|
|
618
732
|
if (!profile && urlUserId && env.DB) {
|
|
619
733
|
try {
|
|
620
734
|
profile = await env.DB.prepare(
|
|
@@ -760,7 +874,7 @@ export default {
|
|
|
760
874
|
},
|
|
761
875
|
|
|
762
876
|
// ── Cron Handler — Intelligence Agent ────────────────────────────────────────
|
|
763
|
-
async scheduled(event, env, ctx) {
|
|
877
|
+
async scheduled(event: any, env: Env, ctx: ExecutionContext) {
|
|
764
878
|
const cron = event.cron;
|
|
765
879
|
const isMonthly = cron === '0 3 1 * *';
|
|
766
880
|
|
|
@@ -768,9 +882,9 @@ export default {
|
|
|
768
882
|
},
|
|
769
883
|
|
|
770
884
|
// ── Queue Consumer — Retry de eventos com falha ───────────────────────────────
|
|
771
|
-
async queue(batch, env) {
|
|
885
|
+
async queue(batch: any, env: Env) {
|
|
772
886
|
for (const message of batch.messages) {
|
|
773
|
-
const { eventType, payload, platform
|
|
887
|
+
const { eventType, payload, platform } = message.body as { eventType: string; payload: TrackPayload; platform: string; attempt?: number };
|
|
774
888
|
|
|
775
889
|
|
|
776
890
|
try {
|
|
@@ -779,8 +893,8 @@ export default {
|
|
|
779
893
|
if (platform === 'tiktok') await sendTikTokApi(env, eventType, payload, null, null);
|
|
780
894
|
|
|
781
895
|
message.ack();
|
|
782
|
-
} catch (err) {
|
|
783
|
-
console.error(`[Queue] Falha ao reprocessar ${platform}/${eventType}:`, err
|
|
896
|
+
} catch (err: any) {
|
|
897
|
+
console.error(`[Queue] Falha ao reprocessar ${platform}/${eventType}:`, err?.message || String(err));
|
|
784
898
|
message.retry();
|
|
785
899
|
}
|
|
786
900
|
}
|