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
|
@@ -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,25 +478,96 @@ 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));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ── recordLtvFeedback — fecha o ciclo preditivo com valor real de compra ─────
|
|
487
|
+
// Chamado em background quando um Purchase chega com payload.value > 0.
|
|
488
|
+
// Atualiza user_profiles + ltv_ab_assignments + ltv_ab_variations em cascata.
|
|
489
|
+
export async function recordLtvFeedback(env: Env, userId: string, realValue: number): Promise<void> {
|
|
490
|
+
if (!env.DB || !userId || !realValue || realValue <= 0) return;
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
// 1. Busca predicted_ltv_value atual do perfil
|
|
494
|
+
const profile = await env.DB.prepare(`
|
|
495
|
+
SELECT predicted_ltv_value FROM user_profiles WHERE user_id = ?
|
|
496
|
+
`).bind(userId).first();
|
|
497
|
+
|
|
498
|
+
// accuracy = 1 - |pred-real|/real (0–1, mesmo padrão do A/B test accuracy_score)
|
|
499
|
+
const predictedValue = profile?.predicted_ltv_value;
|
|
500
|
+
const ltv_accuracy = (predictedValue !== null && predictedValue !== undefined)
|
|
501
|
+
? Math.max(0, Math.round((1 - Math.abs(Number(predictedValue) - realValue) / Math.max(realValue, 1)) * 100) / 100)
|
|
502
|
+
: null;
|
|
503
|
+
|
|
504
|
+
// 2. Grava valor real + accuracy no perfil
|
|
505
|
+
await env.DB.prepare(`
|
|
506
|
+
UPDATE user_profiles
|
|
507
|
+
SET real_ltv_value = ?,
|
|
508
|
+
ltv_accuracy = ?,
|
|
509
|
+
ltv_feedback_at = datetime('now'),
|
|
510
|
+
updated_at = datetime('now')
|
|
511
|
+
WHERE user_id = ?
|
|
512
|
+
`).bind(realValue, ltv_accuracy, userId).run();
|
|
513
|
+
|
|
514
|
+
// 3. Fecha assignment do A/B test mais recente não convertido (janela 60 dias)
|
|
515
|
+
const assignment = await env.DB.prepare(`
|
|
516
|
+
SELECT id, variation_id, predicted_ltv
|
|
517
|
+
FROM ltv_ab_assignments
|
|
518
|
+
WHERE user_id = ?
|
|
519
|
+
AND converted = 0
|
|
520
|
+
AND assigned_at > datetime('now', '-60 days')
|
|
521
|
+
ORDER BY assigned_at DESC
|
|
522
|
+
LIMIT 1
|
|
523
|
+
`).bind(userId).first();
|
|
524
|
+
|
|
525
|
+
if (!assignment) return;
|
|
526
|
+
|
|
527
|
+
// 3a. Marca assignment como convertido
|
|
528
|
+
await env.DB.prepare(`
|
|
529
|
+
UPDATE ltv_ab_assignments
|
|
530
|
+
SET converted = 1,
|
|
531
|
+
real_revenue = ?,
|
|
532
|
+
converted_at = datetime('now')
|
|
533
|
+
WHERE id = ?
|
|
534
|
+
`).bind(realValue, (assignment as any).id).run();
|
|
535
|
+
|
|
536
|
+
// 3b. Atualiza métricas acumuladas da variação (running average — safe para concorrência D1)
|
|
537
|
+
const predLtv = (assignment as any).predicted_ltv || 0;
|
|
538
|
+
const indivAcc = Math.max(0, 1 - Math.abs(predLtv - realValue) / Math.max(realValue, 1));
|
|
539
|
+
|
|
540
|
+
await env.DB.prepare(`
|
|
541
|
+
UPDATE ltv_ab_variations
|
|
542
|
+
SET total_purchases = total_purchases + 1,
|
|
543
|
+
sum_real_revenue = sum_real_revenue + ?,
|
|
544
|
+
avg_real_revenue = (sum_real_revenue + ?) / (total_purchases + 1),
|
|
545
|
+
accuracy_score = ROUND(
|
|
546
|
+
(COALESCE(accuracy_score, 0) * total_purchases + ?) / (total_purchases + 1),
|
|
547
|
+
4
|
|
548
|
+
)
|
|
549
|
+
WHERE id = ?
|
|
550
|
+
`).bind(realValue, realValue, indivAcc, (assignment as any).variation_id).run();
|
|
551
|
+
|
|
552
|
+
} catch (err: any) {
|
|
553
|
+
console.error('[LTV-Feedback] recordLtvFeedback error:', err?.message || String(err));
|
|
443
554
|
}
|
|
444
555
|
}
|
|
445
556
|
|
|
446
557
|
// ── Feedback Loop — Log de falhas e métricas de saúde ────────────────────────
|
|
447
558
|
|
|
448
|
-
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> {
|
|
449
560
|
try {
|
|
450
561
|
await DB.prepare(`
|
|
451
562
|
INSERT INTO api_failures (platform, event_name, error_code, error_message, event_id, raw_payload, retry_count, final_status)
|
|
452
563
|
VALUES (?, ?, ?, ?, ?, ?, 0, 'failed')
|
|
453
564
|
`).bind(platform, eventName, String(errorCode), errorMessage, eventId, rawPayload).run();
|
|
454
|
-
} catch (err) {
|
|
455
|
-
console.error('Failed to log API failure:', err
|
|
565
|
+
} catch (err: any) {
|
|
566
|
+
console.error('Failed to log API failure:', err?.message || String(err));
|
|
456
567
|
}
|
|
457
568
|
}
|
|
458
569
|
|
|
459
|
-
export async function getHealthMetrics(DB, platform, hours = 24) {
|
|
570
|
+
export async function getHealthMetrics(DB: D1Database, platform: string, hours: number = 24): Promise<HealthMetrics> {
|
|
460
571
|
try {
|
|
461
572
|
const failures = await DB.prepare(`
|
|
462
573
|
SELECT COUNT(*) as count, error_code
|
|
@@ -471,30 +582,30 @@ export async function getHealthMetrics(DB, platform, hours = 24) {
|
|
|
471
582
|
WHERE platform = ? AND created_at > datetime('now', '-${hours} hours')
|
|
472
583
|
`).bind(platform).first();
|
|
473
584
|
|
|
474
|
-
const totalFailed = failures.results?.reduce((sum, f) => sum + f.count, 0) || 0;
|
|
475
|
-
const successRate = totalSent?.count > 0
|
|
476
|
-
? ((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
|
|
477
588
|
: 100;
|
|
478
589
|
|
|
479
590
|
return {
|
|
480
591
|
platform,
|
|
481
592
|
hours,
|
|
482
|
-
events_sent: totalSent?.count || 0,
|
|
593
|
+
events_sent: (totalSent as any)?.count || 0,
|
|
483
594
|
events_failed: totalFailed,
|
|
484
595
|
success_rate: successRate,
|
|
485
|
-
errors_detected: (failures.results || []).map(f => ({ code: f.error_code, count: f.count })),
|
|
486
|
-
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'] : [],
|
|
487
598
|
};
|
|
488
|
-
} catch (err) {
|
|
489
|
-
console.error('Failed to get health metrics:', err
|
|
599
|
+
} catch (err: any) {
|
|
600
|
+
console.error('Failed to get health metrics:', err?.message || String(err));
|
|
490
601
|
return { platform, hours, events_sent: 0, events_failed: 0, success_rate: 0, errors_detected: [], issues: ['metrics_unavailable'] };
|
|
491
602
|
}
|
|
492
603
|
}
|
|
493
604
|
|
|
494
|
-
export async function generateDailyReport(DB) {
|
|
605
|
+
export async function generateDailyReport(DB: D1Database): Promise<DailyReport[]> {
|
|
495
606
|
const platforms = ['meta', 'ga4', 'tiktok', 'pinterest', 'reddit'];
|
|
496
607
|
const today = new Date().toISOString().split('T')[0];
|
|
497
|
-
const reports = [];
|
|
608
|
+
const reports: DailyReport[] = [];
|
|
498
609
|
|
|
499
610
|
for (const platform of platforms) {
|
|
500
611
|
const metrics = await getHealthMetrics(DB, platform, 24);
|
|
@@ -509,8 +620,8 @@ export async function generateDailyReport(DB) {
|
|
|
509
620
|
JSON.stringify(metrics.errors_detected), JSON.stringify(metrics.issues)
|
|
510
621
|
).run();
|
|
511
622
|
reports.push({ platform, status: 'ok' });
|
|
512
|
-
} catch (err) {
|
|
513
|
-
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));
|
|
514
625
|
reports.push({ platform, status: 'failed' });
|
|
515
626
|
}
|
|
516
627
|
}
|
|
@@ -518,14 +629,14 @@ export async function generateDailyReport(DB) {
|
|
|
518
629
|
return reports;
|
|
519
630
|
}
|
|
520
631
|
|
|
521
|
-
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> {
|
|
522
633
|
if (!DB) return;
|
|
523
634
|
try {
|
|
524
635
|
await DB.prepare(`
|
|
525
636
|
INSERT INTO intelligence_logs (run_type, platform, check_type, status, current_value, expected_value, message, alert_sent)
|
|
526
637
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
527
638
|
`).bind(runType, platform, checkType, status, String(currentValue ?? ''), String(expectedValue ?? ''), message, alertSent ? 1 : 0).run();
|
|
528
|
-
} catch (err) {
|
|
529
|
-
console.error('logIntelligence error:', err
|
|
639
|
+
} catch (err: any) {
|
|
640
|
+
console.error('logIntelligence error:', err?.message || String(err));
|
|
530
641
|
}
|
|
531
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
|
}
|