cdp-edge 1.23.3 → 1.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +2 -2
- package/docs/PixelBuilder-Documentacao-Completa (2).docx +0 -0
|
@@ -4,11 +4,55 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { extractFeatures, predictWithWeights, loadActiveWeights } from './logistic.js';
|
|
7
|
+
import { Env, TrackPayload } from '../../types.js';
|
|
8
|
+
|
|
9
|
+
// ── Tipos ───────────────────────────────────────────────────────────────────────
|
|
10
|
+
export interface LtvResult {
|
|
11
|
+
score: number;
|
|
12
|
+
class: string;
|
|
13
|
+
value: number;
|
|
14
|
+
source?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AbVariation {
|
|
18
|
+
id: number;
|
|
19
|
+
test_id: number;
|
|
20
|
+
name: string;
|
|
21
|
+
system_prompt: string;
|
|
22
|
+
weight: number;
|
|
23
|
+
is_control: number;
|
|
24
|
+
total_assigned?: number;
|
|
25
|
+
accuracy_score?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AbTestCreate {
|
|
29
|
+
name: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
min_sample?: number;
|
|
32
|
+
variations: Array<{
|
|
33
|
+
name: string;
|
|
34
|
+
system_prompt?: string;
|
|
35
|
+
weight?: number;
|
|
36
|
+
is_control?: boolean;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AutoDecideResult {
|
|
41
|
+
decided: boolean;
|
|
42
|
+
reason?: string;
|
|
43
|
+
test_id?: number;
|
|
44
|
+
test_name?: string;
|
|
45
|
+
winner_id?: number;
|
|
46
|
+
winner_name?: string;
|
|
47
|
+
improvement?: string;
|
|
48
|
+
is_control_winner?: boolean;
|
|
49
|
+
winning_prompt?: string;
|
|
50
|
+
}
|
|
7
51
|
|
|
8
52
|
// Cache key para o teste ativo (KV — evita hit no D1 a cada request /track)
|
|
9
53
|
const AB_LTV_CACHE_KEY = 'ab_ltv_active_test';
|
|
10
54
|
|
|
11
|
-
// ── Prompt especializado para imóveis
|
|
55
|
+
// ── Prompt especializado para imóveis ───────────────────────────────────────
|
|
12
56
|
// Ativado automaticamente quando property_lat/lng estão presentes no payload.
|
|
13
57
|
// Override por A/B test tem prioridade sobre este prompt.
|
|
14
58
|
const REAL_ESTATE_PROMPT = `You are a real estate lead scoring expert for the Brazilian market.
|
|
@@ -26,7 +70,7 @@ Scoring rules (apply additively):
|
|
|
26
70
|
No explanation. JSON only.`;
|
|
27
71
|
|
|
28
72
|
// ── predictLtv — Heurística em 5 dimensões (0-100 pts) ───────────────────────
|
|
29
|
-
export async function predictLtv(env, payload, request, customSystemPrompt = null) {
|
|
73
|
+
export async function predictLtv(env: Env, payload: TrackPayload, request: Request | null, customSystemPrompt: string | null = null): Promise<LtvResult> {
|
|
30
74
|
// ── Tentar modelo treinado (regressão logística real) ─────────────────────
|
|
31
75
|
// Se existir modelo ativo no KV/D1, usa-o em vez da heurística manual.
|
|
32
76
|
// Fallback automático para heurística se modelo não disponível.
|
|
@@ -34,10 +78,10 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
|
|
|
34
78
|
const model = await loadActiveWeights(env);
|
|
35
79
|
if (model?.weights?.length) {
|
|
36
80
|
const hour = new Date().getUTCHours();
|
|
37
|
-
const country = (payload.country || request?.cf?.country || '').toUpperCase();
|
|
81
|
+
const country = (payload.country || (request as any)?.cf?.country || '').toUpperCase();
|
|
38
82
|
const features = extractFeatures({
|
|
39
83
|
utm_source: payload.utmSource,
|
|
40
|
-
engagement_score: parseFloat(payload.engagementScore || 0),
|
|
84
|
+
engagement_score: parseFloat(String(payload.engagementScore || '0')),
|
|
41
85
|
intention_level: payload.intentionLevel,
|
|
42
86
|
days_since_lead: 0, // evento atual = recência máxima
|
|
43
87
|
has_email: !!payload.email,
|
|
@@ -49,7 +93,7 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
|
|
|
49
93
|
const score100 = predictWithWeights(model, features);
|
|
50
94
|
const ltvClass = score100 >= 70 ? 'High' : score100 >= 40 ? 'Medium' : 'Low';
|
|
51
95
|
const ltvMultiplier = score100 >= 70 ? 3.5 : score100 >= 40 ? 1.8 : 0.8;
|
|
52
|
-
const productValue = payload.value ? parseFloat(payload.value) : 0;
|
|
96
|
+
const productValue = payload.value ? parseFloat(String(payload.value)) : 0;
|
|
53
97
|
const baseValue = productValue > 0 ? productValue : 197;
|
|
54
98
|
const predictedValue = Math.round(baseValue * ltvMultiplier * 100) / 100;
|
|
55
99
|
|
|
@@ -60,14 +104,14 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
|
|
|
60
104
|
let score = 0;
|
|
61
105
|
|
|
62
106
|
// 1. Engajamento browser (0–30)
|
|
63
|
-
const engScore = parseFloat(payload.engagementScore || 0);
|
|
64
|
-
const userScore = parseFloat(payload.userScore || 0);
|
|
107
|
+
const engScore = parseFloat(String(payload.engagementScore || '0'));
|
|
108
|
+
const userScore = parseFloat(String((payload as any).userScore || '0'));
|
|
65
109
|
score += Math.min(15, Math.round((engScore / 5) * 15));
|
|
66
110
|
score += Math.min(15, Math.round((userScore / 100) * 15));
|
|
67
111
|
|
|
68
112
|
// 2. Origem de tráfego (0–25)
|
|
69
113
|
const src = (payload.utmSource || '').toLowerCase();
|
|
70
|
-
const utm_score_map = {
|
|
114
|
+
const utm_score_map: Record<string, number> = {
|
|
71
115
|
facebook: 25, instagram: 25, meta: 25,
|
|
72
116
|
google: 22, youtube: 22, tiktok: 20,
|
|
73
117
|
email: 18, sms: 18,
|
|
@@ -77,8 +121,8 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
|
|
|
77
121
|
|
|
78
122
|
// 3. Contexto de rede (0–15)
|
|
79
123
|
const hour = new Date().getUTCHours();
|
|
80
|
-
const country = (payload.country || request
|
|
81
|
-
const org = String(request.cf?.asOrganization || '').toLowerCase();
|
|
124
|
+
const country = (payload.country || (request as any)?.cf?.country || '').toUpperCase();
|
|
125
|
+
const org = String((request as any).cf?.asOrganization || '').toLowerCase();
|
|
82
126
|
|
|
83
127
|
const isHighConvTime = hour >= 21 || hour <= 2;
|
|
84
128
|
score += isHighConvTime ? 8 : (hour >= 12 && hour <= 20 ? 4 : 1);
|
|
@@ -101,27 +145,28 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
|
|
|
101
145
|
if (payload.firstName) score += 2;
|
|
102
146
|
|
|
103
147
|
// 5b. Tipo de evento imobiliário (0–15) — sinais de intenção de compra física
|
|
104
|
-
const evType = (payload.eventType || '').toLowerCase();
|
|
148
|
+
const evType = ((payload as any).eventType || '').toLowerCase();
|
|
105
149
|
if (evType === 'customizeproduct') score += 15; // simulação de financiamento → intenção máxima
|
|
106
150
|
else if (evType === 'findlocation') score += 10; // viu mapa/localização → visita física iminente
|
|
107
151
|
else if (evType === 'addtowishlist') score += 8; // favoritou → interesse persistente
|
|
108
152
|
|
|
109
153
|
// 6. Proximidade ao imóvel físico (0–15) — apenas quando distância calculada
|
|
110
|
-
const distKm = parseFloat(payload.distanceKm
|
|
154
|
+
const distKm = parseFloat(String(payload.distanceKm || (payload as any).user_distance_km || '-1'));
|
|
111
155
|
if (distKm >= 0) {
|
|
112
|
-
if (distKm <
|
|
113
|
-
else if (distKm < 15)
|
|
114
|
-
else if (distKm < 30)
|
|
115
|
-
else if (distKm < 60)
|
|
156
|
+
if (distKm < 5) score += 15;
|
|
157
|
+
else if (distKm < 15) score += 10;
|
|
158
|
+
else if (distKm < 30) score += 6;
|
|
159
|
+
else if (distKm < 60) score += 3;
|
|
116
160
|
// > 60km: sem bônus — lead distante precisa de argumento diferente
|
|
117
|
-
} else if (payload.property_lat || payload.propertyLat) {
|
|
161
|
+
} else if (payload.property_lat || (payload as any).propertyLat) {
|
|
118
162
|
// Coords no payload mas distância não resolvida: pequeno bônus por intenção de rota
|
|
119
163
|
score += 3;
|
|
120
164
|
}
|
|
121
165
|
|
|
122
166
|
score = Math.min(100, score);
|
|
123
167
|
|
|
124
|
-
let ltvClass
|
|
168
|
+
let ltvClass: string;
|
|
169
|
+
let ltvMultiplier: number;
|
|
125
170
|
if (score >= 70) {
|
|
126
171
|
ltvClass = 'High'; ltvMultiplier = 3.5;
|
|
127
172
|
} else if (score >= 40) {
|
|
@@ -130,7 +175,7 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
|
|
|
130
175
|
ltvClass = 'Low'; ltvMultiplier = 0.8;
|
|
131
176
|
}
|
|
132
177
|
|
|
133
|
-
const productValue = payload.value ? parseFloat(payload.value) : 0;
|
|
178
|
+
const productValue = payload.value ? parseFloat(String(payload.value)) : 0;
|
|
134
179
|
const baseValue = productValue > 0 ? productValue : 197;
|
|
135
180
|
const predictedValue = Math.round(baseValue * ltvMultiplier * 100) / 100;
|
|
136
181
|
|
|
@@ -138,16 +183,16 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
|
|
|
138
183
|
let aiAdjustment = 0;
|
|
139
184
|
if (env.AI && score >= 40) {
|
|
140
185
|
try {
|
|
141
|
-
const isRealEstate = !!(payload.property_lat || payload.propertyLat);
|
|
186
|
+
const isRealEstate = !!(payload.property_lat || (payload as any).propertyLat);
|
|
142
187
|
const systemContent = customSystemPrompt ||
|
|
143
188
|
(isRealEstate
|
|
144
189
|
? REAL_ESTATE_PROMPT
|
|
145
190
|
: 'You are a conversion rate expert. Reply ONLY with a JSON object {"adjustment": <number between -10 and 10>} based on the lead data provided. No explanation.');
|
|
146
191
|
|
|
147
|
-
const userContext = {
|
|
148
|
-
utm_source:
|
|
149
|
-
intention:
|
|
150
|
-
engagement:
|
|
192
|
+
const userContext: Record<string, any> = {
|
|
193
|
+
utm_source: payload.utmSource,
|
|
194
|
+
intention: intentionLevel,
|
|
195
|
+
engagement: engScore,
|
|
151
196
|
hour_utc: hour,
|
|
152
197
|
country,
|
|
153
198
|
has_email: !!payload.email,
|
|
@@ -155,11 +200,11 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
|
|
|
155
200
|
};
|
|
156
201
|
if (isRealEstate) {
|
|
157
202
|
userContext.event_type = 'real_estate_schedule';
|
|
158
|
-
userContext.distance_km = payload.distanceKm
|
|
159
|
-
userContext.distance_bucket = payload.distanceBucket
|
|
160
|
-
userContext.scroll_score = payload.scrollScore || payload.scroll_score || 0;
|
|
161
|
-
userContext.time_level = payload.timeLevel || payload.
|
|
162
|
-
userContext.intent_score = payload.intent_score || 'high';
|
|
203
|
+
userContext.distance_km = payload.distanceKm || (payload as any).user_distance_km || 'unknown';
|
|
204
|
+
userContext.distance_bucket = payload.distanceBucket || 'unknown';
|
|
205
|
+
userContext.scroll_score = payload.scrollScore || (payload as any).scroll_score || 0;
|
|
206
|
+
userContext.time_level = payload.timeLevel || (payload as any).timeLevel || 'unknown';
|
|
207
|
+
userContext.intent_score = payload.intentScoreNum || payload.intent_score || 'high';
|
|
163
208
|
userContext.hour_brt = (hour - 3 + 24) % 24; // UTC-3 aproximado
|
|
164
209
|
}
|
|
165
210
|
|
|
@@ -168,7 +213,7 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
|
|
|
168
213
|
{ role: 'user', content: JSON.stringify(userContext) },
|
|
169
214
|
];
|
|
170
215
|
const aiRes = await env.AI.run('@cf/ibm-granite/granite-4.0-h-micro', { messages: prompt, max_tokens: 32 });
|
|
171
|
-
const parsed = JSON.parse(aiRes.response.trim());
|
|
216
|
+
const parsed = JSON.parse((aiRes as any).response.trim());
|
|
172
217
|
if (typeof parsed.adjustment === 'number') {
|
|
173
218
|
aiAdjustment = Math.max(-10, Math.min(10, parsed.adjustment));
|
|
174
219
|
}
|
|
@@ -183,13 +228,13 @@ export async function predictLtv(env, payload, request, customSystemPrompt = nul
|
|
|
183
228
|
}
|
|
184
229
|
|
|
185
230
|
// ── getLtvAbVariation — busca variação ativa do A/B test ─────────────────────
|
|
186
|
-
export async function getLtvAbVariation(env) {
|
|
231
|
+
export async function getLtvAbVariation(env: Env): Promise<AbVariation | null> {
|
|
187
232
|
if (!env.DB) return null;
|
|
188
233
|
|
|
189
234
|
try {
|
|
190
|
-
let testData = null;
|
|
235
|
+
let testData: any = null;
|
|
191
236
|
if (env.GEO_CACHE) {
|
|
192
|
-
const cached = await env.GEO_CACHE.get(AB_LTV_CACHE_KEY, 'json');
|
|
237
|
+
const cached = await env.GEO_CACHE.get(AB_LTV_CACHE_KEY, 'json') as any;
|
|
193
238
|
if (cached) testData = cached;
|
|
194
239
|
}
|
|
195
240
|
|
|
@@ -214,7 +259,7 @@ export async function getLtvAbVariation(env) {
|
|
|
214
259
|
|
|
215
260
|
if (!testData || testData.length === 0) return null;
|
|
216
261
|
|
|
217
|
-
const totalWeight = testData.reduce((s, v) => s + (v.weight || 0.5), 0);
|
|
262
|
+
const totalWeight = testData.reduce((s: number, v: AbVariation) => s + (v.weight || 0.5), 0);
|
|
218
263
|
let rand = Math.random() * totalWeight;
|
|
219
264
|
for (const variation of testData) {
|
|
220
265
|
rand -= (variation.weight || 0.5);
|
|
@@ -222,14 +267,14 @@ export async function getLtvAbVariation(env) {
|
|
|
222
267
|
}
|
|
223
268
|
return testData[testData.length - 1];
|
|
224
269
|
|
|
225
|
-
} catch (err) {
|
|
226
|
-
console.error('[AB-LTV] getLtvAbVariation error:', err
|
|
270
|
+
} catch (err: any) {
|
|
271
|
+
console.error('[AB-LTV] getLtvAbVariation error:', err?.message || String(err));
|
|
227
272
|
return null;
|
|
228
273
|
}
|
|
229
274
|
}
|
|
230
275
|
|
|
231
276
|
// ── recordAbAssignment — registra variação usada para um lead ─────────────────
|
|
232
|
-
export async function recordAbAssignment(env, userId, variationId, testId, predictedLtv, predictedClass, emailHash) {
|
|
277
|
+
export async function recordAbAssignment(env: Env, userId: string, variationId: number, testId: number, predictedLtv: number | null, predictedClass: string | null, emailHash: string | null): Promise<void> {
|
|
233
278
|
if (!env.DB) return;
|
|
234
279
|
try {
|
|
235
280
|
await env.DB.prepare(`
|
|
@@ -238,17 +283,17 @@ export async function recordAbAssignment(env, userId, variationId, testId, predi
|
|
|
238
283
|
`).bind(testId, variationId, userId, emailHash || null, predictedLtv || null, predictedClass || null).run();
|
|
239
284
|
|
|
240
285
|
await env.DB.prepare(`UPDATE ltv_ab_variations SET total_assigned = total_assigned + 1 WHERE id = ?`).bind(variationId).run();
|
|
241
|
-
} catch (err) {
|
|
242
|
-
console.error('[AB-LTV] recordAbAssignment error:', err
|
|
286
|
+
} catch (err: any) {
|
|
287
|
+
console.error('[AB-LTV] recordAbAssignment error:', err?.message || String(err));
|
|
243
288
|
}
|
|
244
289
|
}
|
|
245
290
|
|
|
246
291
|
// ── POST /api/ltv/ab-test/create ─────────────────────────────────────────────
|
|
247
|
-
export async function handleLtvAbTestCreate(env, request, headers) {
|
|
292
|
+
export async function handleLtvAbTestCreate(env: Env, request: Request, headers: Headers): Promise<Response> {
|
|
248
293
|
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
249
294
|
|
|
250
|
-
let body;
|
|
251
|
-
try { body = await request.json(); }
|
|
295
|
+
let body: AbTestCreate;
|
|
296
|
+
try { body = await request.json() as AbTestCreate; }
|
|
252
297
|
catch { return new Response(JSON.stringify({ error: 'JSON inválido no body' }), { status: 400, headers }); }
|
|
253
298
|
|
|
254
299
|
const { name, description, min_sample = 100, variations } = body;
|
|
@@ -259,7 +304,7 @@ export async function handleLtvAbTestCreate(env, request, headers) {
|
|
|
259
304
|
|
|
260
305
|
const running = await env.DB.prepare(`SELECT id FROM ltv_ab_tests WHERE status = 'running' LIMIT 1`).first();
|
|
261
306
|
if (running) {
|
|
262
|
-
return new Response(JSON.stringify({ error: 'Já existe um teste em andamento.', running_test_id: running.id }), { status: 409, headers });
|
|
307
|
+
return new Response(JSON.stringify({ error: 'Já existe um teste em andamento.', running_test_id: (running as any).id }), { status: 409, headers });
|
|
263
308
|
}
|
|
264
309
|
|
|
265
310
|
try {
|
|
@@ -276,28 +321,28 @@ export async function handleLtvAbTestCreate(env, request, headers) {
|
|
|
276
321
|
INSERT INTO ltv_ab_tests (name, description, status, min_sample, created_at) VALUES (?, ?, 'running', ?, ?)
|
|
277
322
|
`).bind(name, description || null, min_sample, now).run();
|
|
278
323
|
|
|
279
|
-
const testId = testRes.meta?.last_row_id;
|
|
324
|
+
const testId = (testRes as any).meta?.last_row_id;
|
|
280
325
|
if (!testId) throw new Error('Falha ao criar o teste no D1');
|
|
281
326
|
|
|
282
|
-
const createdVariations = [];
|
|
327
|
+
const createdVariations: Array<{ id: number; name: string; weight: number; is_control: boolean }> = [];
|
|
283
328
|
for (const v of variations) {
|
|
284
329
|
const vRes = await env.DB.prepare(`
|
|
285
330
|
INSERT INTO ltv_ab_variations (test_id, name, system_prompt, weight, is_control, created_at) VALUES (?, ?, ?, ?, ?, ?)
|
|
286
331
|
`).bind(testId, v.name, v.system_prompt, v.weight || 0.5, v.is_control ? 1 : 0, now).run();
|
|
287
|
-
createdVariations.push({ id: vRes.meta?.last_row_id, name: v.name, weight: v.weight, is_control: !!v.is_control });
|
|
332
|
+
createdVariations.push({ id: (vRes as any).meta?.last_row_id, name: v.name, weight: v.weight || 0.5, is_control: !!v.is_control });
|
|
288
333
|
}
|
|
289
334
|
|
|
290
335
|
if (env.GEO_CACHE) await env.GEO_CACHE.delete(AB_LTV_CACHE_KEY);
|
|
291
336
|
|
|
292
337
|
return new Response(JSON.stringify({ success: true, test_id: testId, name, status: 'running', min_sample, variations: createdVariations, started_at: now }), { status: 201, headers });
|
|
293
|
-
} catch (err) {
|
|
294
|
-
console.error('[AB-LTV] create error:', err
|
|
295
|
-
return new Response(JSON.stringify({ error: err
|
|
338
|
+
} catch (err: any) {
|
|
339
|
+
console.error('[AB-LTV] create error:', err?.message || String(err));
|
|
340
|
+
return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
|
|
296
341
|
}
|
|
297
342
|
}
|
|
298
343
|
|
|
299
344
|
// ── GET /api/ltv/ab-test/list ─────────────────────────────────────────────────
|
|
300
|
-
export async function handleLtvAbTestList(env, request, headers) {
|
|
345
|
+
export async function handleLtvAbTestList(env: Env, request: Request, headers: Headers): Promise<Response> {
|
|
301
346
|
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
302
347
|
|
|
303
348
|
const url = new URL(request.url);
|
|
@@ -306,7 +351,7 @@ export async function handleLtvAbTestList(env, request, headers) {
|
|
|
306
351
|
|
|
307
352
|
try {
|
|
308
353
|
const cond = status ? 'WHERE t.status = ?' : '';
|
|
309
|
-
const bindings = status ? [status, limit] : [limit];
|
|
354
|
+
const bindings: (string | number)[] = status ? [status, limit] : [limit];
|
|
310
355
|
|
|
311
356
|
const tests = await env.DB.prepare(`
|
|
312
357
|
SELECT t.id, t.name, t.description, t.status, t.winner_id,
|
|
@@ -322,14 +367,14 @@ export async function handleLtvAbTestList(env, request, headers) {
|
|
|
322
367
|
`).bind(...bindings).all();
|
|
323
368
|
|
|
324
369
|
return new Response(JSON.stringify({ success: true, total: (tests.results || []).length, tests: tests.results || [] }), { status: 200, headers });
|
|
325
|
-
} catch (err) {
|
|
326
|
-
console.error('[AB-LTV] list error:', err
|
|
327
|
-
return new Response(JSON.stringify({ error: err
|
|
370
|
+
} catch (err: any) {
|
|
371
|
+
console.error('[AB-LTV] list error:', err?.message || String(err));
|
|
372
|
+
return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
|
|
328
373
|
}
|
|
329
374
|
}
|
|
330
375
|
|
|
331
376
|
// ── GET /api/ltv/ab-test/results ─────────────────────────────────────────────
|
|
332
|
-
export async function handleLtvAbTestResults(env, request, headers) {
|
|
377
|
+
export async function handleLtvAbTestResults(env: Env, request: Request, headers: Headers): Promise<Response> {
|
|
333
378
|
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
334
379
|
|
|
335
380
|
const url = new URL(request.url);
|
|
@@ -342,15 +387,15 @@ export async function handleLtvAbTestResults(env, request, headers) {
|
|
|
342
387
|
const testRes = await env.DB.prepare(`SELECT id, name, status, min_sample, winner_id, started_at FROM ltv_ab_tests ${cond} LIMIT 1`).bind(...testBind).first();
|
|
343
388
|
if (!testRes) return new Response(JSON.stringify({ error: 'Nenhum teste encontrado' }), { status: 404, headers });
|
|
344
389
|
|
|
345
|
-
const perf = await env.DB.prepare(`SELECT * FROM v_ab_test_performance WHERE test_id = ?`).bind(testRes.id).all();
|
|
390
|
+
const perf = await env.DB.prepare(`SELECT * FROM v_ab_test_performance WHERE test_id = ?`).bind((testRes as any).id).all();
|
|
346
391
|
const variations = perf.results || [];
|
|
347
|
-
const ready = variations.every(v => (v.total_assigned || 0) >= testRes.min_sample);
|
|
348
|
-
let recommendation = null;
|
|
392
|
+
const ready = variations.every((v: any) => (v.total_assigned || 0) >= (testRes as any).min_sample);
|
|
393
|
+
let recommendation: any = null;
|
|
349
394
|
|
|
350
395
|
if (ready && variations.length > 0) {
|
|
351
|
-
const best = variations.reduce((a, b) => (b.accuracy_score || 0) > (a.accuracy_score || 0) ? b : a);
|
|
352
|
-
const control = variations.find(v => v.is_control);
|
|
353
|
-
const improvement = control ? ((best.accuracy_score || 0) - (control.accuracy_score || 0)) * 100 : null;
|
|
396
|
+
const best = variations.reduce((a: any, b: any) => (Number(b.accuracy_score) || 0) > (Number(a.accuracy_score) || 0) ? b : a);
|
|
397
|
+
const control = variations.find((v: any) => v.is_control);
|
|
398
|
+
const improvement = control ? ((Number(best.accuracy_score) || 0) - (Number(control.accuracy_score) || 0)) * 100 : null;
|
|
354
399
|
recommendation = {
|
|
355
400
|
winner_variation_id: best.variation_id, winner_variation_name: best.variation_name,
|
|
356
401
|
accuracy_score: best.accuracy_score, improvement_vs_control: improvement ? `+${improvement.toFixed(1)}%` : null,
|
|
@@ -360,20 +405,20 @@ export async function handleLtvAbTestResults(env, request, headers) {
|
|
|
360
405
|
|
|
361
406
|
return new Response(JSON.stringify({
|
|
362
407
|
success: true,
|
|
363
|
-
test: { id: testRes.id, name: testRes.name, status: testRes.status, min_sample: testRes.min_sample, started_at: testRes.started_at, is_ready: ready },
|
|
408
|
+
test: { id: (testRes as any).id, name: (testRes as any).name, status: (testRes as any).status, min_sample: (testRes as any).min_sample, started_at: (testRes as any).started_at, is_ready: ready },
|
|
364
409
|
variations, recommendation,
|
|
365
410
|
}), { status: 200, headers });
|
|
366
|
-
} catch (err) {
|
|
367
|
-
console.error('[AB-LTV] results error:', err
|
|
368
|
-
return new Response(JSON.stringify({ error: err
|
|
411
|
+
} catch (err: any) {
|
|
412
|
+
console.error('[AB-LTV] results error:', err?.message || String(err));
|
|
413
|
+
return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
|
|
369
414
|
}
|
|
370
415
|
}
|
|
371
416
|
|
|
372
417
|
// ── POST /api/ltv/ab-test/winner ──────────────────────────────────────────────
|
|
373
|
-
export async function handleLtvAbTestWinner(env, request, headers) {
|
|
418
|
+
export async function handleLtvAbTestWinner(env: Env, request: Request, headers: Headers): Promise<Response> {
|
|
374
419
|
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
375
420
|
|
|
376
|
-
let body;
|
|
421
|
+
let body: any;
|
|
377
422
|
try { body = await request.json(); }
|
|
378
423
|
catch { return new Response(JSON.stringify({ error: 'JSON inválido' }), { status: 400, headers }); }
|
|
379
424
|
|
|
@@ -390,22 +435,22 @@ export async function handleLtvAbTestWinner(env, request, headers) {
|
|
|
390
435
|
if (env.GEO_CACHE) await env.GEO_CACHE.delete(AB_LTV_CACHE_KEY);
|
|
391
436
|
|
|
392
437
|
return new Response(JSON.stringify({
|
|
393
|
-
success: true, test_id, winner_variation_id: variation_id, winner_name: variation.name,
|
|
394
|
-
is_control: variation.is_control === 1, winning_prompt: variation.system_prompt,
|
|
395
|
-
message: variation.is_control === 1
|
|
438
|
+
success: true, test_id, winner_variation_id: variation_id, winner_name: (variation as any).name,
|
|
439
|
+
is_control: (variation as any).is_control === 1, winning_prompt: (variation as any).system_prompt,
|
|
440
|
+
message: (variation as any).is_control === 1
|
|
396
441
|
? 'O prompt original (controle) venceu. Nenhuma alteração necessária.'
|
|
397
442
|
: 'Novo prompt vencedor identificado. Copie o campo winning_prompt e aplique ao predictLtv() como novo default.',
|
|
398
443
|
}), { status: 200, headers });
|
|
399
|
-
} catch (err) {
|
|
400
|
-
console.error('[AB-LTV] winner error:', err
|
|
401
|
-
return new Response(JSON.stringify({ error: err
|
|
444
|
+
} catch (err: any) {
|
|
445
|
+
console.error('[AB-LTV] winner error:', err?.message || String(err));
|
|
446
|
+
return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
|
|
402
447
|
}
|
|
403
448
|
}
|
|
404
449
|
|
|
405
450
|
// ── autoDecideAbWinner — declara winner automaticamente via cron ──────────────
|
|
406
451
|
// Critério: todas as variações com amostra >= min_sample
|
|
407
452
|
// E diferença de accuracy_score >= 5pp entre melhor e controle
|
|
408
|
-
export async function autoDecideAbWinner(env) {
|
|
453
|
+
export async function autoDecideAbWinner(env: Env): Promise<AutoDecideResult> {
|
|
409
454
|
if (!env.DB) return { decided: false, reason: 'no_db' };
|
|
410
455
|
|
|
411
456
|
try {
|
|
@@ -419,24 +464,24 @@ export async function autoDecideAbWinner(env) {
|
|
|
419
464
|
// Buscar performance das variações
|
|
420
465
|
const perf = await env.DB.prepare(
|
|
421
466
|
`SELECT * FROM v_ab_test_performance WHERE test_id = ?`
|
|
422
|
-
).bind(test.id).all();
|
|
467
|
+
).bind((test as any).id).all();
|
|
423
468
|
|
|
424
469
|
const variations = perf.results || [];
|
|
425
470
|
if (variations.length < 2) return { decided: false, reason: 'insufficient_variations' };
|
|
426
471
|
|
|
427
472
|
// Verificar se todas têm amostra suficiente
|
|
428
|
-
const allReady = variations.every(v => (v.total_assigned || 0) >= test.min_sample);
|
|
473
|
+
const allReady = variations.every((v: any) => (v.total_assigned || 0) >= (test as any).min_sample);
|
|
429
474
|
if (!allReady) {
|
|
430
|
-
const minAssigned = Math.min(...variations.map(v => v.total_assigned || 0));
|
|
431
|
-
return { decided: false, reason: `sample_insufficient (${minAssigned}/${test.min_sample})` };
|
|
475
|
+
const minAssigned = Math.min(...variations.map((v: any) => v.total_assigned || 0));
|
|
476
|
+
return { decided: false, reason: `sample_insufficient (${minAssigned}/${(test as any).min_sample})` };
|
|
432
477
|
}
|
|
433
478
|
|
|
434
479
|
// Encontrar melhor e controle
|
|
435
|
-
const best = variations.reduce((a, b) => (b.accuracy_score || 0) > (a.accuracy_score || 0) ? b : a);
|
|
436
|
-
const control = variations.find(v => v.is_control) || variations[0];
|
|
480
|
+
const best = variations.reduce((a: any, b: any) => (b.accuracy_score || 0) > (a.accuracy_score || 0) ? b : a);
|
|
481
|
+
const control = variations.find((v: any) => v.is_control) || variations[0];
|
|
437
482
|
|
|
438
|
-
const bestScore = parseFloat(best.accuracy_score || 0);
|
|
439
|
-
const controlScore = parseFloat(control.accuracy_score || 0);
|
|
483
|
+
const bestScore = parseFloat(String(best.accuracy_score) || '0');
|
|
484
|
+
const controlScore = parseFloat(String(control.accuracy_score) || '0');
|
|
440
485
|
const diff = bestScore - controlScore;
|
|
441
486
|
|
|
442
487
|
// Empate técnico → controle vence (determinístico)
|
|
@@ -455,17 +500,17 @@ export async function autoDecideAbWinner(env) {
|
|
|
455
500
|
|
|
456
501
|
return {
|
|
457
502
|
decided: true,
|
|
458
|
-
test_id: test.id,
|
|
459
|
-
test_name: test.name,
|
|
460
|
-
winner_id: best.variation_id,
|
|
461
|
-
winner_name: best.variation_name,
|
|
503
|
+
test_id: (test as any).id,
|
|
504
|
+
test_name: (test as any).name,
|
|
505
|
+
winner_id: typeof best.variation_id === 'number' ? best.variation_id : undefined,
|
|
506
|
+
winner_name: typeof best.variation_name === 'string' ? best.variation_name : undefined,
|
|
462
507
|
improvement: `+${(diff * 100).toFixed(1)}pp`,
|
|
463
508
|
is_control_winner: best.variation_id === control.variation_id,
|
|
464
|
-
winning_prompt: best.system_prompt ||
|
|
509
|
+
winning_prompt: String(best.system_prompt || ''),
|
|
465
510
|
};
|
|
466
511
|
|
|
467
|
-
} catch (err) {
|
|
468
|
-
console.error('[AB-LTV] autoDecide error:', err
|
|
469
|
-
return { decided: false, reason: err
|
|
512
|
+
} catch (err: any) {
|
|
513
|
+
console.error('[AB-LTV] autoDecide error:', err?.message || String(err));
|
|
514
|
+
return { decided: false, reason: err?.message || String(err) };
|
|
470
515
|
}
|
|
471
516
|
}
|