cdp-edge 2.2.4 → 2.3.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 +44 -2
- package/bin/cdp-edge.js +10 -1
- package/contracts/types.ts +81 -0
- package/dist/commands/install.js +6 -1
- package/docs/whatsapp-ctwa.md +3 -2
- package/package.json +7 -4
- package/server-edge-tracker/{index.js → index.ts} +91 -82
- package/server-edge-tracker/modules/{db.js → db.ts} +116 -76
- package/server-edge-tracker/modules/dispatch/{ga4.js → ga4.ts} +12 -10
- package/server-edge-tracker/modules/dispatch/{meta.js → meta.ts} +35 -28
- 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} +48 -40
- package/server-edge-tracker/modules/ml/{logistic.js → logistic.ts} +44 -19
- package/server-edge-tracker/modules/ml/{ltv.js → ltv.ts} +135 -90
- package/server-edge-tracker/modules/ml/{matchquality.js → matchquality.ts} +70 -26
- package/server-edge-tracker/modules/ml/{segmentation.js → segmentation.ts} +109 -48
- package/server-edge-tracker/modules/{utils.js → utils.ts} +41 -22
- package/server-edge-tracker/types.ts +251 -0
- package/server-edge-tracker/wrangler.toml +8 -8
- package/docs/PixelBuilder-Documentacao-Completa (2).docx +0 -0
|
@@ -5,9 +5,49 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { sha256, normalizePhone, normalizeCity } from './utils.js';
|
|
8
|
+
import { Env, TrackPayload } from '../types.js';
|
|
9
|
+
import { D1Database } from '@cloudflare/workers-types';
|
|
10
|
+
|
|
11
|
+
// ── Tipos ───────────────────────────────────────────────────────────────────────
|
|
12
|
+
export interface GeoData {
|
|
13
|
+
country: string | null;
|
|
14
|
+
continent: string | null;
|
|
15
|
+
asn: string | null;
|
|
16
|
+
asOrg: string | null;
|
|
17
|
+
colo: string | null;
|
|
18
|
+
city: string | null;
|
|
19
|
+
region: string | null;
|
|
20
|
+
regionCode: string | null;
|
|
21
|
+
postalCode: string | null;
|
|
22
|
+
latitude: number | null;
|
|
23
|
+
longitude: number | null;
|
|
24
|
+
timezone: string | null;
|
|
25
|
+
metroCode: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface LtvResult {
|
|
29
|
+
value: number;
|
|
30
|
+
class: string;
|
|
31
|
+
score?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface HealthMetrics {
|
|
35
|
+
platform: string;
|
|
36
|
+
hours: number;
|
|
37
|
+
events_sent: number;
|
|
38
|
+
events_failed: number;
|
|
39
|
+
success_rate: number;
|
|
40
|
+
errors_detected: Array<{ code: string; count: number }>;
|
|
41
|
+
issues: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface DailyReport {
|
|
45
|
+
platform: string;
|
|
46
|
+
status: string;
|
|
47
|
+
}
|
|
8
48
|
|
|
9
49
|
// ── saveLead — inserir evento de conversão ────────────────────────────────────
|
|
10
|
-
export async function saveLead(env, eventName, payload, request, platform = 'website') {
|
|
50
|
+
export async function saveLead(env: Env, eventName: string, payload: TrackPayload, request: Request, platform: string = 'website'): Promise<void> {
|
|
11
51
|
if (!env.DB) return;
|
|
12
52
|
try {
|
|
13
53
|
const {
|
|
@@ -36,7 +76,7 @@ export async function saveLead(env, eventName, payload, request, platform = 'web
|
|
|
36
76
|
lastName || null,
|
|
37
77
|
city || null,
|
|
38
78
|
state || null,
|
|
39
|
-
(country || request.cf?.country || null),
|
|
79
|
+
(country || (request as any).cf?.country || null),
|
|
40
80
|
fbp || null,
|
|
41
81
|
fbc || null,
|
|
42
82
|
userId || null,
|
|
@@ -46,22 +86,22 @@ export async function saveLead(env, eventName, payload, request, platform = 'web
|
|
|
46
86
|
utmContent || null,
|
|
47
87
|
utmTerm || null,
|
|
48
88
|
pageUrl || null,
|
|
49
|
-
value !== undefined ? parseFloat(value) : null,
|
|
89
|
+
value !== undefined ? parseFloat(String(value)) : null,
|
|
50
90
|
currency || 'BRL',
|
|
51
91
|
request.headers.get('CF-Connecting-IP') || null,
|
|
52
92
|
platform,
|
|
53
93
|
botScore || 0,
|
|
54
|
-
engagementScore !== undefined ? parseFloat(engagementScore) : null,
|
|
94
|
+
engagementScore !== undefined ? parseFloat(String(engagementScore)) : null,
|
|
55
95
|
intentionLevel || null,
|
|
56
96
|
utmRestored ? 1 : 0,
|
|
57
97
|
).run();
|
|
58
|
-
} catch (err) {
|
|
59
|
-
console.error('D1 saveLead error:', err
|
|
98
|
+
} catch (err: any) {
|
|
99
|
+
console.error('D1 saveLead error:', err?.message || String(err));
|
|
60
100
|
}
|
|
61
101
|
}
|
|
62
102
|
|
|
63
103
|
// ── calculateCohortLabel ──────────────────────────────────────────────────────
|
|
64
|
-
export function calculateCohortLabel(score, eventName) {
|
|
104
|
+
export function calculateCohortLabel(score: number, eventName: string): string {
|
|
65
105
|
if (eventName === 'Purchase') return 'buyer_lookalike';
|
|
66
106
|
if (score >= 80) return 'high_intent';
|
|
67
107
|
if (score >= 30) return 'nurture';
|
|
@@ -69,7 +109,7 @@ export function calculateCohortLabel(score, eventName) {
|
|
|
69
109
|
}
|
|
70
110
|
|
|
71
111
|
// ── upsertProfile — acumula cookies/scores entre visitas ─────────────────────
|
|
72
|
-
export async function upsertProfile(env, eventName, payload, request) {
|
|
112
|
+
export async function upsertProfile(env: Env, eventName: string, payload: TrackPayload, request: Request): Promise<void> {
|
|
73
113
|
if (!env.DB || !payload.userId) return;
|
|
74
114
|
try {
|
|
75
115
|
const {
|
|
@@ -79,7 +119,7 @@ export async function upsertProfile(env, eventName, payload, request) {
|
|
|
79
119
|
engagementScore, userScore,
|
|
80
120
|
} = payload;
|
|
81
121
|
|
|
82
|
-
const scoreMap = { PageView: 5, ViewContent: 10, ScrollDepth: 3, TimeOnPage: 5, Lead: 30, InitiateCheckout: 50, Purchase: 100 };
|
|
122
|
+
const scoreMap: Record<string, number> = { PageView: 5, ViewContent: 10, ScrollDepth: 3, TimeOnPage: 5, Lead: 30, InitiateCheckout: 50, Purchase: 100 };
|
|
83
123
|
const eventScore = scoreMap[eventName] || 2;
|
|
84
124
|
|
|
85
125
|
const behaviorBonus = userScore
|
|
@@ -120,23 +160,23 @@ export async function upsertProfile(env, eventName, payload, request) {
|
|
|
120
160
|
gaClientId || null,
|
|
121
161
|
city || null,
|
|
122
162
|
state || null,
|
|
123
|
-
(country || request.cf?.country || null),
|
|
163
|
+
(country || (request as any).cf?.country || null),
|
|
124
164
|
totalDelta,
|
|
125
165
|
calculateCohortLabel(totalDelta, eventName),
|
|
126
166
|
).run();
|
|
127
|
-
} catch (err) {
|
|
128
|
-
console.error('D1 upsertProfile error:', err
|
|
167
|
+
} catch (err: any) {
|
|
168
|
+
console.error('D1 upsertProfile error:', err?.message || String(err));
|
|
129
169
|
}
|
|
130
170
|
}
|
|
131
171
|
|
|
132
172
|
// ── resolveDeviceGraph — Cross-Device Identity ────────────────────────────────
|
|
133
|
-
export async function resolveDeviceGraph(DB, currentUserId, email, phone) {
|
|
173
|
+
export async function resolveDeviceGraph(DB: D1Database, currentUserId: string, email?: string | null, phone?: string | null): Promise<void> {
|
|
134
174
|
if (!DB || !currentUserId) return;
|
|
135
175
|
if (!email && !phone) return;
|
|
136
176
|
|
|
137
177
|
try {
|
|
138
|
-
const conditions = [];
|
|
139
|
-
const bindings = [];
|
|
178
|
+
const conditions: string[] = [];
|
|
179
|
+
const bindings: (string | number)[] = [];
|
|
140
180
|
|
|
141
181
|
if (email) {
|
|
142
182
|
conditions.push('email = ?');
|
|
@@ -166,7 +206,7 @@ export async function resolveDeviceGraph(DB, currentUserId, email, phone) {
|
|
|
166
206
|
|
|
167
207
|
for (const match of rows.results) {
|
|
168
208
|
const emailMatch = email && match.email &&
|
|
169
|
-
email.toLowerCase().trim() === match.email.toLowerCase().trim();
|
|
209
|
+
email.toLowerCase().trim() === (match.email as string).toLowerCase().trim();
|
|
170
210
|
const phoneMatch = phone && match.phone && (() => {
|
|
171
211
|
const a = String(phone).replace(/\D/g, '');
|
|
172
212
|
const b = String(match.phone).replace(/\D/g, '');
|
|
@@ -177,7 +217,7 @@ export async function resolveDeviceGraph(DB, currentUserId, email, phone) {
|
|
|
177
217
|
|
|
178
218
|
const matchType = emailMatch && phoneMatch ? 'email+phone' : (emailMatch ? 'email' : 'phone');
|
|
179
219
|
const matchConfidence = emailMatch && phoneMatch ? 0.99 : (emailMatch ? 0.95 : 0.85);
|
|
180
|
-
const primary = match.user_id;
|
|
220
|
+
const primary = match.user_id as string;
|
|
181
221
|
const secondary = currentUserId;
|
|
182
222
|
|
|
183
223
|
await DB.prepare(`
|
|
@@ -188,13 +228,13 @@ export async function resolveDeviceGraph(DB, currentUserId, email, phone) {
|
|
|
188
228
|
|
|
189
229
|
console.log(`[DeviceGraph] Linked ${secondary} → ${primary} via ${matchType} (confidence: ${matchConfidence})`);
|
|
190
230
|
}
|
|
191
|
-
} catch (err) {
|
|
192
|
-
console.error('resolveDeviceGraph error:', err
|
|
231
|
+
} catch (err: any) {
|
|
232
|
+
console.error('resolveDeviceGraph error:', err?.message || String(err));
|
|
193
233
|
}
|
|
194
234
|
}
|
|
195
235
|
|
|
196
236
|
// ── fireAutomation — dispara regras de automação (WA/Email) ──────────────────
|
|
197
|
-
export async function fireAutomation(env, eventName, leadId, payload) {
|
|
237
|
+
export async function fireAutomation(env: Env, eventName: string, leadId: number | null, payload: TrackPayload): Promise<void> {
|
|
198
238
|
if (!env.DB) return;
|
|
199
239
|
|
|
200
240
|
try {
|
|
@@ -209,20 +249,20 @@ export async function fireAutomation(env, eventName, leadId, payload) {
|
|
|
209
249
|
|
|
210
250
|
if (!rules || rules.length === 0) return;
|
|
211
251
|
|
|
212
|
-
const vars = {
|
|
213
|
-
name: String(payload.firstName || payload.name || ''),
|
|
252
|
+
const vars: Record<string, string> = {
|
|
253
|
+
name: String(payload.firstName || (payload as any).name || ''),
|
|
214
254
|
email: String(payload.email || ''),
|
|
215
255
|
phone: String(payload.phone || ''),
|
|
216
|
-
campaign: String(payload.
|
|
256
|
+
campaign: String(payload.utmCampaign || payload.utm_campaign || ''),
|
|
217
257
|
intention: String(payload.intentionLevel || payload.intention_level || ''),
|
|
218
258
|
};
|
|
219
259
|
|
|
220
|
-
const interpolate = (tpl) =>
|
|
260
|
+
const interpolate = (tpl: string) =>
|
|
221
261
|
tpl.replace(/\{\{(\w+)\}\}/g, (_, k) => vars[k] ?? '');
|
|
222
262
|
|
|
223
263
|
for (const rule of rules) {
|
|
224
|
-
const message = interpolate(rule.message_template);
|
|
225
|
-
const subject = rule.subject_template ? interpolate(rule.subject_template) : null;
|
|
264
|
+
const message = interpolate(rule.message_template as string);
|
|
265
|
+
const subject = rule.subject_template ? interpolate(rule.subject_template as string) : null;
|
|
226
266
|
|
|
227
267
|
try {
|
|
228
268
|
if (rule.channel === 'whatsapp' && payload.phone && env.WHATSAPP_ACCESS_TOKEN && env.WHATSAPP_PHONE_NUMBER_ID) {
|
|
@@ -238,7 +278,7 @@ export async function fireAutomation(env, eventName, leadId, payload) {
|
|
|
238
278
|
);
|
|
239
279
|
const waData = await waRes.json();
|
|
240
280
|
const status = waRes.ok ? 'sent' : 'failed';
|
|
241
|
-
const meta = waRes.ok ? (waData.messages?.[0]?.id ?? null) : JSON.stringify(waData);
|
|
281
|
+
const meta = waRes.ok ? ((waData as any).messages?.[0]?.id ?? null) : JSON.stringify(waData);
|
|
242
282
|
await env.DB.prepare(
|
|
243
283
|
`INSERT INTO messaging_history (lead_id, channel, recipient, subject, content, status, meta) VALUES (?1,?2,?3,?4,?5,?6,?7)`
|
|
244
284
|
).bind(leadId, 'whatsapp', e164, null, message, status, meta).run();
|
|
@@ -256,22 +296,22 @@ export async function fireAutomation(env, eventName, leadId, payload) {
|
|
|
256
296
|
});
|
|
257
297
|
const resendData = await resendRes.json();
|
|
258
298
|
const status = resendRes.ok ? 'sent' : 'failed';
|
|
259
|
-
const meta = resendRes.ok ? (resendData.id ?? null) : JSON.stringify(resendData);
|
|
299
|
+
const meta = resendRes.ok ? ((resendData as any).id ?? null) : JSON.stringify(resendData);
|
|
260
300
|
await env.DB.prepare(
|
|
261
301
|
`INSERT INTO messaging_history (lead_id, channel, recipient, subject, content, status, meta) VALUES (?1,?2,?3,?4,?5,?6,?7)`
|
|
262
302
|
).bind(leadId, 'email', payload.email, subject, message, status, meta).run();
|
|
263
303
|
}
|
|
264
|
-
} catch (err) {
|
|
265
|
-
console.error(`[Automation] rule ${rule.id} error:`, err
|
|
304
|
+
} catch (err: any) {
|
|
305
|
+
console.error(`[Automation] rule ${(rule as any).id} error:`, err?.message || String(err));
|
|
266
306
|
}
|
|
267
307
|
}
|
|
268
|
-
} catch (err) {
|
|
269
|
-
console.error('[Automation] fireAutomation error:', err
|
|
308
|
+
} catch (err: any) {
|
|
309
|
+
console.error('[Automation] fireAutomation error:', err?.message || String(err));
|
|
270
310
|
}
|
|
271
311
|
}
|
|
272
312
|
|
|
273
313
|
// ── getProfileByEmail ─────────────────────────────────────────────────────────
|
|
274
|
-
export async function getProfileByEmail(env, email) {
|
|
314
|
+
export async function getProfileByEmail(env: Env, email: string): Promise<any | null> {
|
|
275
315
|
if (!env.DB || !email) return null;
|
|
276
316
|
try {
|
|
277
317
|
return await env.DB.prepare(
|
|
@@ -283,14 +323,14 @@ export async function getProfileByEmail(env, email) {
|
|
|
283
323
|
}
|
|
284
324
|
|
|
285
325
|
// ── enrichGeoFromEdge — enriquece payload com dados de geolocalização ─────────
|
|
286
|
-
export async function enrichGeoFromEdge(request, env, payload) {
|
|
287
|
-
const cf = request.cf || {};
|
|
326
|
+
export async function enrichGeoFromEdge(request: Request, env: Env, payload: TrackPayload): Promise<GeoData> {
|
|
327
|
+
const cf = (request as any).cf || {};
|
|
288
328
|
const ip = request.headers.get('CF-Connecting-IP') || '';
|
|
289
329
|
|
|
290
|
-
let geoData = null;
|
|
330
|
+
let geoData: GeoData | null = null;
|
|
291
331
|
if (env.GEO_CACHE && ip) {
|
|
292
332
|
try {
|
|
293
|
-
const cached = await env.GEO_CACHE.get(`geo:${ip}`, 'json');
|
|
333
|
+
const cached = await env.GEO_CACHE.get(`geo:${ip}`, 'json') as GeoData | null;
|
|
294
334
|
if (cached) geoData = cached;
|
|
295
335
|
} catch {}
|
|
296
336
|
}
|
|
@@ -323,13 +363,13 @@ export async function enrichGeoFromEdge(request, env, payload) {
|
|
|
323
363
|
payload.city = payload.city || geoData.city;
|
|
324
364
|
payload.state = payload.state || geoData.regionCode;
|
|
325
365
|
payload.zip = payload.zip || geoData.postalCode;
|
|
326
|
-
payload.geo = geoData;
|
|
366
|
+
(payload as any).geo = geoData;
|
|
327
367
|
|
|
328
368
|
return geoData;
|
|
329
369
|
}
|
|
330
370
|
|
|
331
371
|
// ── writeAuditLog — grava evento no R2 ───────────────────────────────────────
|
|
332
|
-
export async function writeAuditLog(env, eventName, payload, geoData) {
|
|
372
|
+
export async function writeAuditLog(env: Env, eventName: string, payload: TrackPayload, geoData: GeoData | null): Promise<void> {
|
|
333
373
|
if (!env.AUDIT_LOGS) return;
|
|
334
374
|
try {
|
|
335
375
|
const now = new Date();
|
|
@@ -360,14 +400,14 @@ export async function writeAuditLog(env, eventName, payload, geoData) {
|
|
|
360
400
|
await env.AUDIT_LOGS.put(key, JSON.stringify(log), {
|
|
361
401
|
httpMetadata: { contentType: 'application/json' },
|
|
362
402
|
});
|
|
363
|
-
} catch (err) {
|
|
364
|
-
console.error('[R2 Audit] Error:', err
|
|
403
|
+
} catch (err: any) {
|
|
404
|
+
console.error('[R2 Audit] Error:', err?.message || String(err));
|
|
365
405
|
}
|
|
366
406
|
}
|
|
367
407
|
|
|
368
408
|
// ── generateEdgeFingerprint ───────────────────────────────────────────────────
|
|
369
|
-
export async function generateEdgeFingerprint(request) {
|
|
370
|
-
const asn = String(request.cf?.asn || '0');
|
|
409
|
+
export async function generateEdgeFingerprint(request: Request): Promise<string | undefined> {
|
|
410
|
+
const asn = String((request as any).cf?.asn || '0');
|
|
371
411
|
const lang = (request.headers.get('Accept-Language') || 'unknown').split(',')[0].trim();
|
|
372
412
|
const ua = request.headers.get('User-Agent') || '';
|
|
373
413
|
|
|
@@ -386,7 +426,7 @@ export async function generateEdgeFingerprint(request) {
|
|
|
386
426
|
}
|
|
387
427
|
|
|
388
428
|
// ── saveEdgeFingerprint ───────────────────────────────────────────────────────
|
|
389
|
-
export async function saveEdgeFingerprint(DB, fingerprint, userId, payload) {
|
|
429
|
+
export async function saveEdgeFingerprint(DB: D1Database, fingerprint: string | undefined, userId: string | undefined, payload: TrackPayload): Promise<void> {
|
|
390
430
|
if (!DB || !fingerprint) return;
|
|
391
431
|
const { utmSource, utmMedium, utmCampaign, utmContent, utmTerm } = payload;
|
|
392
432
|
if (!utmSource) return;
|
|
@@ -404,13 +444,13 @@ export async function saveEdgeFingerprint(DB, fingerprint, userId, payload) {
|
|
|
404
444
|
utmContent || null,
|
|
405
445
|
utmTerm || null,
|
|
406
446
|
).run();
|
|
407
|
-
} catch (err) {
|
|
408
|
-
console.error('saveEdgeFingerprint error:', err
|
|
447
|
+
} catch (err: any) {
|
|
448
|
+
console.error('saveEdgeFingerprint error:', err?.message || String(err));
|
|
409
449
|
}
|
|
410
450
|
}
|
|
411
451
|
|
|
412
452
|
// ── resurrectUTM ──────────────────────────────────────────────────────────────
|
|
413
|
-
export async function resurrectUTM(DB, fingerprint) {
|
|
453
|
+
export async function resurrectUTM(DB: D1Database, fingerprint: string | undefined): Promise<any | null> {
|
|
414
454
|
if (!DB || !fingerprint) return null;
|
|
415
455
|
try {
|
|
416
456
|
return await DB.prepare(`
|
|
@@ -428,7 +468,7 @@ export async function resurrectUTM(DB, fingerprint) {
|
|
|
428
468
|
}
|
|
429
469
|
|
|
430
470
|
// ── upsertLtvProfile — persiste LTV no perfil ────────────────────────────────
|
|
431
|
-
export async function upsertLtvProfile(env, userId, ltv) {
|
|
471
|
+
export async function upsertLtvProfile(env: Env, userId: string, ltv: LtvResult): Promise<void> {
|
|
432
472
|
if (!env.DB || !userId) return;
|
|
433
473
|
try {
|
|
434
474
|
await env.DB.prepare(`
|
|
@@ -438,15 +478,15 @@ export async function upsertLtvProfile(env, userId, ltv) {
|
|
|
438
478
|
updated_at = datetime('now')
|
|
439
479
|
WHERE user_id = ?
|
|
440
480
|
`).bind(ltv.class, ltv.value, userId).run();
|
|
441
|
-
} catch (err) {
|
|
442
|
-
console.error('upsertLtvProfile error:', err
|
|
481
|
+
} catch (err: any) {
|
|
482
|
+
console.error('upsertLtvProfile error:', err?.message || String(err));
|
|
443
483
|
}
|
|
444
484
|
}
|
|
445
485
|
|
|
446
486
|
// ── recordLtvFeedback — fecha o ciclo preditivo com valor real de compra ─────
|
|
447
487
|
// Chamado em background quando um Purchase chega com payload.value > 0.
|
|
448
488
|
// Atualiza user_profiles + ltv_ab_assignments + ltv_ab_variations em cascata.
|
|
449
|
-
export async function recordLtvFeedback(env, userId, realValue) {
|
|
489
|
+
export async function recordLtvFeedback(env: Env, userId: string, realValue: number): Promise<void> {
|
|
450
490
|
if (!env.DB || !userId || !realValue || realValue <= 0) return;
|
|
451
491
|
|
|
452
492
|
try {
|
|
@@ -458,7 +498,7 @@ export async function recordLtvFeedback(env, userId, realValue) {
|
|
|
458
498
|
// accuracy = 1 - |pred-real|/real (0–1, mesmo padrão do A/B test accuracy_score)
|
|
459
499
|
const predictedValue = profile?.predicted_ltv_value;
|
|
460
500
|
const ltv_accuracy = (predictedValue !== null && predictedValue !== undefined)
|
|
461
|
-
? Math.max(0, Math.round((1 - Math.abs(predictedValue - realValue) / Math.max(realValue, 1)) * 100) / 100)
|
|
501
|
+
? Math.max(0, Math.round((1 - Math.abs(Number(predictedValue) - realValue) / Math.max(realValue, 1)) * 100) / 100)
|
|
462
502
|
: null;
|
|
463
503
|
|
|
464
504
|
// 2. Grava valor real + accuracy no perfil
|
|
@@ -491,10 +531,10 @@ export async function recordLtvFeedback(env, userId, realValue) {
|
|
|
491
531
|
real_revenue = ?,
|
|
492
532
|
converted_at = datetime('now')
|
|
493
533
|
WHERE id = ?
|
|
494
|
-
`).bind(realValue, assignment.id).run();
|
|
534
|
+
`).bind(realValue, (assignment as any).id).run();
|
|
495
535
|
|
|
496
536
|
// 3b. Atualiza métricas acumuladas da variação (running average — safe para concorrência D1)
|
|
497
|
-
const predLtv = assignment.predicted_ltv || 0;
|
|
537
|
+
const predLtv = (assignment as any).predicted_ltv || 0;
|
|
498
538
|
const indivAcc = Math.max(0, 1 - Math.abs(predLtv - realValue) / Math.max(realValue, 1));
|
|
499
539
|
|
|
500
540
|
await env.DB.prepare(`
|
|
@@ -507,27 +547,27 @@ export async function recordLtvFeedback(env, userId, realValue) {
|
|
|
507
547
|
4
|
|
508
548
|
)
|
|
509
549
|
WHERE id = ?
|
|
510
|
-
`).bind(realValue, realValue, indivAcc, assignment.variation_id).run();
|
|
550
|
+
`).bind(realValue, realValue, indivAcc, (assignment as any).variation_id).run();
|
|
511
551
|
|
|
512
|
-
} catch (err) {
|
|
513
|
-
console.error('[LTV-Feedback] recordLtvFeedback error:', err
|
|
552
|
+
} catch (err: any) {
|
|
553
|
+
console.error('[LTV-Feedback] recordLtvFeedback error:', err?.message || String(err));
|
|
514
554
|
}
|
|
515
555
|
}
|
|
516
556
|
|
|
517
557
|
// ── Feedback Loop — Log de falhas e métricas de saúde ────────────────────────
|
|
518
558
|
|
|
519
|
-
export async function logApiFailure(DB, platform, eventName, errorCode, errorMessage, eventId, rawPayload) {
|
|
559
|
+
export async function logApiFailure(DB: D1Database, platform: string, eventName: string, errorCode: string | number, errorMessage: string, eventId: string, rawPayload: string): Promise<void> {
|
|
520
560
|
try {
|
|
521
561
|
await DB.prepare(`
|
|
522
562
|
INSERT INTO api_failures (platform, event_name, error_code, error_message, event_id, raw_payload, retry_count, final_status)
|
|
523
563
|
VALUES (?, ?, ?, ?, ?, ?, 0, 'failed')
|
|
524
564
|
`).bind(platform, eventName, String(errorCode), errorMessage, eventId, rawPayload).run();
|
|
525
|
-
} catch (err) {
|
|
526
|
-
console.error('Failed to log API failure:', err
|
|
565
|
+
} catch (err: any) {
|
|
566
|
+
console.error('Failed to log API failure:', err?.message || String(err));
|
|
527
567
|
}
|
|
528
568
|
}
|
|
529
569
|
|
|
530
|
-
export async function getHealthMetrics(DB, platform, hours = 24) {
|
|
570
|
+
export async function getHealthMetrics(DB: D1Database, platform: string, hours: number = 24): Promise<HealthMetrics> {
|
|
531
571
|
try {
|
|
532
572
|
const failures = await DB.prepare(`
|
|
533
573
|
SELECT COUNT(*) as count, error_code
|
|
@@ -542,30 +582,30 @@ export async function getHealthMetrics(DB, platform, hours = 24) {
|
|
|
542
582
|
WHERE platform = ? AND created_at > datetime('now', '-${hours} hours')
|
|
543
583
|
`).bind(platform).first();
|
|
544
584
|
|
|
545
|
-
const totalFailed = failures.results?.reduce((sum, f) => sum + f.count, 0) || 0;
|
|
546
|
-
const successRate = totalSent?.count > 0
|
|
547
|
-
? ((totalSent.count - totalFailed) / totalSent.count) * 100
|
|
585
|
+
const totalFailed = failures.results?.reduce((sum: number, f: any) => sum + f.count, 0) || 0;
|
|
586
|
+
const successRate = (totalSent as any)?.count > 0
|
|
587
|
+
? (((totalSent as any).count - totalFailed) / (totalSent as any).count) * 100
|
|
548
588
|
: 100;
|
|
549
589
|
|
|
550
590
|
return {
|
|
551
591
|
platform,
|
|
552
592
|
hours,
|
|
553
|
-
events_sent: totalSent?.count || 0,
|
|
593
|
+
events_sent: (totalSent as any)?.count || 0,
|
|
554
594
|
events_failed: totalFailed,
|
|
555
595
|
success_rate: successRate,
|
|
556
|
-
errors_detected: (failures.results || []).map(f => ({ code: f.error_code, count: f.count })),
|
|
557
|
-
issues: totalFailed > (totalSent?.count || 0) * 0.1 ? ['high_error_rate'] : [],
|
|
596
|
+
errors_detected: (failures.results || []).map((f: any) => ({ code: f.error_code, count: f.count })),
|
|
597
|
+
issues: totalFailed > ((totalSent as any)?.count || 0) * 0.1 ? ['high_error_rate'] : [],
|
|
558
598
|
};
|
|
559
|
-
} catch (err) {
|
|
560
|
-
console.error('Failed to get health metrics:', err
|
|
599
|
+
} catch (err: any) {
|
|
600
|
+
console.error('Failed to get health metrics:', err?.message || String(err));
|
|
561
601
|
return { platform, hours, events_sent: 0, events_failed: 0, success_rate: 0, errors_detected: [], issues: ['metrics_unavailable'] };
|
|
562
602
|
}
|
|
563
603
|
}
|
|
564
604
|
|
|
565
|
-
export async function generateDailyReport(DB) {
|
|
605
|
+
export async function generateDailyReport(DB: D1Database): Promise<DailyReport[]> {
|
|
566
606
|
const platforms = ['meta', 'ga4', 'tiktok', 'pinterest', 'reddit'];
|
|
567
607
|
const today = new Date().toISOString().split('T')[0];
|
|
568
|
-
const reports = [];
|
|
608
|
+
const reports: DailyReport[] = [];
|
|
569
609
|
|
|
570
610
|
for (const platform of platforms) {
|
|
571
611
|
const metrics = await getHealthMetrics(DB, platform, 24);
|
|
@@ -580,8 +620,8 @@ export async function generateDailyReport(DB) {
|
|
|
580
620
|
JSON.stringify(metrics.errors_detected), JSON.stringify(metrics.issues)
|
|
581
621
|
).run();
|
|
582
622
|
reports.push({ platform, status: 'ok' });
|
|
583
|
-
} catch (err) {
|
|
584
|
-
console.error(`Failed to generate report for ${platform}:`, err
|
|
623
|
+
} catch (err: any) {
|
|
624
|
+
console.error(`Failed to generate report for ${platform}:`, err?.message || String(err));
|
|
585
625
|
reports.push({ platform, status: 'failed' });
|
|
586
626
|
}
|
|
587
627
|
}
|
|
@@ -589,14 +629,14 @@ export async function generateDailyReport(DB) {
|
|
|
589
629
|
return reports;
|
|
590
630
|
}
|
|
591
631
|
|
|
592
|
-
export async function logIntelligence(DB, runType, platform, checkType, status, currentValue, expectedValue, message, alertSent = false) {
|
|
632
|
+
export async function logIntelligence(DB: D1Database, runType: string, platform: string, checkType: string, status: string, currentValue: any, expectedValue: any, message: string, alertSent: boolean = false): Promise<void> {
|
|
593
633
|
if (!DB) return;
|
|
594
634
|
try {
|
|
595
635
|
await DB.prepare(`
|
|
596
636
|
INSERT INTO intelligence_logs (run_type, platform, check_type, status, current_value, expected_value, message, alert_sent)
|
|
597
637
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
598
638
|
`).bind(runType, platform, checkType, status, String(currentValue ?? ''), String(expectedValue ?? ''), message, alertSent ? 1 : 0).run();
|
|
599
|
-
} catch (err) {
|
|
600
|
-
console.error('logIntelligence error:', err
|
|
639
|
+
} catch (err: any) {
|
|
640
|
+
console.error('logIntelligence error:', err?.message || String(err));
|
|
601
641
|
}
|
|
602
642
|
}
|
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
|
|
6
6
|
import { normalizePhone } from '../utils.js';
|
|
7
7
|
import { logApiFailure } from '../db.js';
|
|
8
|
+
import { Env, TrackPayload } from '../../types.js';
|
|
9
|
+
import { ExecutionContext } from '@cloudflare/workers-types';
|
|
8
10
|
|
|
9
|
-
export async function sendGA4Mp(env, ga4EventName, payload, ctx) {
|
|
11
|
+
export async function sendGA4Mp(env: Env, ga4EventName: string, payload: TrackPayload, ctx: ExecutionContext | null): Promise<{ ok?: boolean; status?: number; skipped?: string; error?: string }> {
|
|
10
12
|
if (!env.GA4_API_SECRET) return { skipped: 'GA4_API_SECRET not set' };
|
|
11
13
|
|
|
12
14
|
const {
|
|
13
|
-
clientId, sessionId,
|
|
15
|
+
gaClientId: clientId, sessionId,
|
|
14
16
|
value, currency, contentName,
|
|
15
17
|
email, phone, firstName,
|
|
16
18
|
orderId,
|
|
@@ -18,13 +20,13 @@ export async function sendGA4Mp(env, ga4EventName, payload, ctx) {
|
|
|
18
20
|
|
|
19
21
|
if (!clientId) return { skipped: 'no clientId' };
|
|
20
22
|
|
|
21
|
-
const eventParams = {
|
|
22
|
-
...(value !== undefined && { value: parseFloat(value) }),
|
|
23
|
+
const eventParams: Record<string, string | number> = {
|
|
24
|
+
...(value !== undefined && { value: parseFloat(String(value)) }),
|
|
23
25
|
...(currency && { currency: String(currency).toUpperCase() }),
|
|
24
26
|
...(contentName && { content_name: contentName }),
|
|
25
27
|
...(orderId && { transaction_id: orderId }),
|
|
26
28
|
...(email && { user_data_email_address: email.toLowerCase().trim() }),
|
|
27
|
-
...(phone && { user_data_phone_number: normalizePhone(phone) }),
|
|
29
|
+
...(phone && { user_data_phone_number: normalizePhone(phone) || '' }),
|
|
28
30
|
...(firstName && { user_data_first_name: firstName.toLowerCase().trim() }),
|
|
29
31
|
...(sessionId && { session_id: sessionId }),
|
|
30
32
|
engagement_time_msec: 100,
|
|
@@ -48,18 +50,18 @@ export async function sendGA4Mp(env, ga4EventName, payload, ctx) {
|
|
|
48
50
|
|
|
49
51
|
if (res.status !== 204) {
|
|
50
52
|
if (env.DB && ctx) {
|
|
51
|
-
ctx.waitUntil(logApiFailure(env.DB, 'ga4', ga4EventName, String(res.status), 'GA4 returned non-204 status',
|
|
53
|
+
ctx.waitUntil(logApiFailure(env.DB, 'ga4', ga4EventName, String(res.status), 'GA4 returned non-204 status', '', JSON.stringify(body)));
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
return res.status === 204 ? { ok: true } : { status: res.status };
|
|
56
|
-
} catch (err) {
|
|
57
|
-
console.error('GA4 MP fetch failed:', err
|
|
58
|
+
} catch (err: any) {
|
|
59
|
+
console.error('GA4 MP fetch failed:', err?.message || String(err));
|
|
58
60
|
|
|
59
61
|
if (env.DB && ctx) {
|
|
60
|
-
ctx.waitUntil(logApiFailure(env.DB, 'ga4', ga4EventName, 'FETCH_ERROR', err
|
|
62
|
+
ctx.waitUntil(logApiFailure(env.DB, 'ga4', ga4EventName, 'FETCH_ERROR', err?.message || String(err), '', JSON.stringify(body)));
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
return { error: err
|
|
65
|
+
return { error: err?.message || String(err) };
|
|
64
66
|
}
|
|
65
67
|
}
|
|
@@ -6,12 +6,19 @@
|
|
|
6
6
|
import { sha256, normalizePhone, normalizeCity } from '../utils.js';
|
|
7
7
|
import { logApiFailure } from '../db.js';
|
|
8
8
|
import { logMatchQuality, autoEnrichPayload } from '../ml/matchquality.js';
|
|
9
|
+
import { Env, TrackPayload } from '../../types.js';
|
|
10
|
+
import { ExecutionContext } from '@cloudflare/workers-types';
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
interface EnrichedPayload {
|
|
13
|
+
payload: TrackPayload;
|
|
14
|
+
recovered: { email: boolean; utm: boolean };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function sendMetaCapi(env: Env, eventName: string, payload: TrackPayload, request: Request | null, ctx: ExecutionContext | null): Promise<any> {
|
|
11
18
|
// Auto-enriquecer payload com dados do Identity Graph antes do envio
|
|
12
19
|
let recovered = { email: false, utm: false };
|
|
13
20
|
if (env.DB && payload) {
|
|
14
|
-
const enriched = await autoEnrichPayload(env, payload);
|
|
21
|
+
const enriched = await autoEnrichPayload(env, payload) as EnrichedPayload;
|
|
15
22
|
payload = enriched.payload;
|
|
16
23
|
recovered = enriched.recovered;
|
|
17
24
|
}
|
|
@@ -25,41 +32,41 @@ export async function sendMetaCapi(env, eventName, payload, request, ctx) {
|
|
|
25
32
|
value, currency,
|
|
26
33
|
contentIds, contentName, contentType, numItems,
|
|
27
34
|
// Dual-layer context — funil avançado + imóveis
|
|
28
|
-
funnel_stage, distance_bucket, intent_score, intent_bucket,
|
|
35
|
+
funnel_stage, distanceBucket: distance_bucket, intentScoreNum: intent_score, intent_bucket,
|
|
29
36
|
ltvScore, ltvClass, metaSignal, metaSignalBucket: metaSignalBucketVal,
|
|
30
37
|
} = payload;
|
|
31
38
|
|
|
32
39
|
const phoneNorm = normalizePhone(phone);
|
|
33
|
-
const countryCode = (country || request
|
|
40
|
+
const countryCode = (country || (request as any)?.cf?.country || 'br').toLowerCase();
|
|
34
41
|
const stateCode = state ? String(state).toLowerCase() : undefined;
|
|
35
42
|
const cityNorm = normalizeCity(city);
|
|
36
43
|
|
|
37
|
-
const userData = {
|
|
38
|
-
...(email && { em: await sha256(email) }),
|
|
39
|
-
...(phoneNorm && { ph: await sha256(phoneNorm) }),
|
|
40
|
-
...(firstName && { fn: await sha256(firstName) }),
|
|
41
|
-
...(lastName && { ln: await sha256(lastName) }),
|
|
42
|
-
...(cityNorm && { ct: await sha256(cityNorm) }),
|
|
43
|
-
...(stateCode && { st: await sha256(stateCode) }),
|
|
44
|
-
...(countryCode && { country: await sha256(countryCode) }),
|
|
45
|
-
...(userId && { external_id: await sha256(String(userId)) }),
|
|
46
|
-
...(zip && { zp: await sha256(zip) }),
|
|
47
|
-
...(dob && { db: await sha256(dob) }),
|
|
44
|
+
const userData: Record<string, string> = {
|
|
45
|
+
...(email && { em: await sha256(email) || '' }),
|
|
46
|
+
...(phoneNorm && { ph: await sha256(phoneNorm) || '' }),
|
|
47
|
+
...(firstName && { fn: await sha256(firstName) || '' }),
|
|
48
|
+
...(lastName && { ln: await sha256(lastName) || '' }),
|
|
49
|
+
...(cityNorm && { ct: await sha256(cityNorm) || '' }),
|
|
50
|
+
...(stateCode && { st: await sha256(stateCode) || '' }),
|
|
51
|
+
...(countryCode && { country: await sha256(countryCode) || '' }),
|
|
52
|
+
...(userId && { external_id: await sha256(String(userId)) || '' }),
|
|
53
|
+
...(zip && { zp: await sha256(zip) || '' }),
|
|
54
|
+
...(dob && { db: await sha256(dob) || '' }),
|
|
48
55
|
...(fbp && { fbp }),
|
|
49
56
|
...(fbc && { fbc }),
|
|
50
|
-
client_ip_address: request
|
|
51
|
-
|| request
|
|
57
|
+
client_ip_address: request?.headers.get('CF-Connecting-IP')
|
|
58
|
+
|| request?.headers.get('X-Forwarded-For')
|
|
52
59
|
|| '',
|
|
53
|
-
client_user_agent: request
|
|
60
|
+
client_user_agent: request?.headers.get('User-Agent') || '',
|
|
54
61
|
};
|
|
55
62
|
|
|
56
|
-
const customData = {
|
|
57
|
-
...(value !== undefined && { value: parseFloat(value) }),
|
|
63
|
+
const customData: Record<string, string | number | string[]> = {
|
|
64
|
+
...(value !== undefined && { value: parseFloat(String(value)) }),
|
|
58
65
|
...(currency && { currency: String(currency).toUpperCase() }),
|
|
59
66
|
...(contentIds && contentIds.length > 0 && { content_ids: contentIds }),
|
|
60
67
|
...(contentName && { content_name: contentName }),
|
|
61
68
|
...(contentType && { content_type: contentType }),
|
|
62
|
-
...(numItems && { num_items: parseInt(numItems) }),
|
|
69
|
+
...(numItems && { num_items: parseInt(String(numItems)) }),
|
|
63
70
|
// Contexto de funil e proximidade — enriquece matching e otimização Meta
|
|
64
71
|
...(funnel_stage && { funnel_stage }),
|
|
65
72
|
...(distance_bucket && { distance_bucket }),
|
|
@@ -81,7 +88,7 @@ export async function sendMetaCapi(env, eventName, payload, request, ctx) {
|
|
|
81
88
|
...(Object.keys(customData).length > 0 && { custom_data: customData }),
|
|
82
89
|
};
|
|
83
90
|
|
|
84
|
-
const requestBody = {
|
|
91
|
+
const requestBody: Record<string, any> = {
|
|
85
92
|
data: [eventPayload],
|
|
86
93
|
access_token: env.META_ACCESS_TOKEN,
|
|
87
94
|
};
|
|
@@ -109,8 +116,8 @@ export async function sendMetaCapi(env, eventName, payload, request, ctx) {
|
|
|
109
116
|
const data = await res.json();
|
|
110
117
|
|
|
111
118
|
if (!res.ok) {
|
|
112
|
-
const errorCode = data.error?.code || String(res.status);
|
|
113
|
-
const errorMessage = data.error?.message || data.error?.error_user_msg || 'Unknown error';
|
|
119
|
+
const errorCode = (data as any).error?.code || String(res.status);
|
|
120
|
+
const errorMessage = (data as any).error?.message || (data as any).error?.error_user_msg || 'Unknown error';
|
|
114
121
|
console.error('Meta CAPI error:', res.status, errorMessage);
|
|
115
122
|
|
|
116
123
|
if (env.DB && ctx) {
|
|
@@ -119,13 +126,13 @@ export async function sendMetaCapi(env, eventName, payload, request, ctx) {
|
|
|
119
126
|
}
|
|
120
127
|
|
|
121
128
|
return data;
|
|
122
|
-
} catch (err) {
|
|
123
|
-
console.error('Meta CAPI fetch failed:', err
|
|
129
|
+
} catch (err: any) {
|
|
130
|
+
console.error('Meta CAPI fetch failed:', err?.message || String(err));
|
|
124
131
|
|
|
125
132
|
if (env.DB && ctx) {
|
|
126
|
-
ctx.waitUntil(logApiFailure(env.DB, 'meta', eventName, 'FETCH_ERROR', err
|
|
133
|
+
ctx.waitUntil(logApiFailure(env.DB, 'meta', eventName, 'FETCH_ERROR', err?.message || String(err), eventPayload.event_id, JSON.stringify(requestBody)));
|
|
127
134
|
}
|
|
128
135
|
|
|
129
|
-
return { error: err
|
|
136
|
+
return { error: err?.message || String(err) };
|
|
130
137
|
}
|
|
131
138
|
}
|