cdp-edge 2.2.5 → 2.3.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 +57 -2
- package/contracts/types.ts +81 -0
- package/docs/whatsapp-ctwa.md +3 -2
- package/package.json +7 -4
- package/server-edge-tracker/.client.env.example +14 -0
- package/server-edge-tracker/deploy-client.js +76 -0
- package/server-edge-tracker/{index.js → index.ts} +93 -84
- package/server-edge-tracker/modules/{db.js → db.ts} +117 -77
- 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} +74 -28
- 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 +255 -0
- package/server-edge-tracker/wrangler.toml +2 -2
- package/docs/PixelBuilder-Documentacao-Completa (2).docx +0 -0
|
@@ -4,24 +4,32 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { sha256, tryParseJson } from '../utils.js';
|
|
7
|
+
import { Env, TrackPayload } from '../../types.js';
|
|
8
|
+
|
|
9
|
+
// ── Tipos ───────────────────────────────────────────────────────────────────────
|
|
10
|
+
export interface FraudResult {
|
|
11
|
+
allowed: boolean;
|
|
12
|
+
score: number;
|
|
13
|
+
reasons: string[];
|
|
14
|
+
action: 'allowed' | 'flagged' | 'dropped';
|
|
15
|
+
}
|
|
7
16
|
|
|
8
|
-
// ── Listas de detecção ────────────────────────────────────────────────────────
|
|
9
17
|
export const DATACENTER_PATTERNS = /amazon|google|microsoft|digitalocean|linode|ovh|vultr|hetzner|contabo|cloudflare|packet|rackspace|leaseweb/i;
|
|
10
18
|
|
|
11
19
|
// ── checkFraudGate — roda ANTES de qualquer processamento de evento ────────────
|
|
12
20
|
// Retorna { allowed, score, reasons, action }
|
|
13
21
|
// Falhas no gate = fail-safe (deixa passar)
|
|
14
|
-
export async function checkFraudGate(env, request, payload) {
|
|
15
|
-
const result = { allowed: true, score: 0, reasons: [], action: 'allowed' };
|
|
22
|
+
export async function checkFraudGate(env: Env, request: Request, payload: TrackPayload): Promise<FraudResult> {
|
|
23
|
+
const result: FraudResult = { allowed: true, score: 0, reasons: [], action: 'allowed' };
|
|
16
24
|
|
|
17
25
|
try {
|
|
18
26
|
const ip = request.headers.get('CF-Connecting-IP') || '';
|
|
19
27
|
const ua = request.headers.get('User-Agent') || '';
|
|
20
|
-
const fingerprint = payload.fingerprint || '';
|
|
28
|
+
const fingerprint = (payload as any).fingerprint || '';
|
|
21
29
|
const email = payload.email || '';
|
|
22
|
-
const botScore = parseInt(payload.botScore || payload.bot_score || 0);
|
|
23
|
-
const asn = String(request.cf?.asOrganization || '').toLowerCase();
|
|
24
|
-
const country = (request.cf?.country || '').toUpperCase();
|
|
30
|
+
const botScore = parseInt(String(payload.botScore || (payload as any).bot_score || 0));
|
|
31
|
+
const asn = String((request as any).cf?.asOrganization || '').toLowerCase();
|
|
32
|
+
const country = ((request as any).cf?.country || '').toUpperCase();
|
|
25
33
|
const acceptLang = request.headers.get('Accept-Language');
|
|
26
34
|
|
|
27
35
|
// 1. KV blocklist check — instantâneo (~0ms)
|
|
@@ -81,22 +89,22 @@ export async function checkFraudGate(env, request, payload) {
|
|
|
81
89
|
|
|
82
90
|
return result;
|
|
83
91
|
|
|
84
|
-
} catch (err) {
|
|
85
|
-
console.error('[Fraud] checkFraudGate error:', err
|
|
92
|
+
} catch (err: any) {
|
|
93
|
+
console.error('[Fraud] checkFraudGate error:', err?.message || String(err));
|
|
86
94
|
return { allowed: true, score: 0, reasons: ['gate_error_fallback'], action: 'allowed' };
|
|
87
95
|
}
|
|
88
96
|
}
|
|
89
97
|
|
|
90
98
|
// ── logFraudSignal — persiste no D1 em background ────────────────────────────
|
|
91
|
-
export async function logFraudSignal(env, request, payload, fraudResult) {
|
|
99
|
+
export async function logFraudSignal(env: Env, request: Request, payload: TrackPayload, fraudResult: FraudResult): Promise<void> {
|
|
92
100
|
if (!env.DB || fraudResult.action === 'allowed') return;
|
|
93
101
|
try {
|
|
94
102
|
const ip = request.headers.get('CF-Connecting-IP') || '';
|
|
95
103
|
const ua = request.headers.get('User-Agent') || '';
|
|
96
|
-
const fingerprint = payload.fingerprint || '';
|
|
97
|
-
const botScore = parseInt(payload.botScore || payload.bot_score || 0);
|
|
98
|
-
const asn = String(request.cf?.asOrganization || '');
|
|
99
|
-
const country = (request.cf?.country || ''
|
|
104
|
+
const fingerprint = (payload as any).fingerprint || '';
|
|
105
|
+
const botScore = parseInt(String(payload.botScore || (payload as any).bot_score || 0));
|
|
106
|
+
const asn = String((request as any).cf?.asOrganization || '');
|
|
107
|
+
const country = (request as any).cf?.country || '';
|
|
100
108
|
const velKey1h = `fraud_velocity:${ip}:h`;
|
|
101
109
|
const vel1h = env.GEO_CACHE ? parseInt(await env.GEO_CACHE.get(velKey1h) || '0') : 0;
|
|
102
110
|
|
|
@@ -130,13 +138,13 @@ export async function logFraudSignal(env, request, payload, fraudResult) {
|
|
|
130
138
|
updated_at = datetime('now')
|
|
131
139
|
`).bind(ip, fraudResult.score, JSON.stringify(fraudResult.reasons)).run().catch(() => {});
|
|
132
140
|
}
|
|
133
|
-
} catch (err) {
|
|
134
|
-
console.error('[Fraud] logFraudSignal error:', err
|
|
141
|
+
} catch (err: any) {
|
|
142
|
+
console.error('[Fraud] logFraudSignal error:', err?.message || String(err));
|
|
135
143
|
}
|
|
136
144
|
}
|
|
137
145
|
|
|
138
146
|
// ── GET /api/fraud/alerts ─────────────────────────────────────────────────────
|
|
139
|
-
export async function handleFraudAlerts(env, request, headers) {
|
|
147
|
+
export async function handleFraudAlerts(env: Env, request: Request, headers: Headers): Promise<Response> {
|
|
140
148
|
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
141
149
|
|
|
142
150
|
const url = new URL(request.url);
|
|
@@ -158,18 +166,18 @@ export async function handleFraudAlerts(env, request, headers) {
|
|
|
158
166
|
LIMIT ?
|
|
159
167
|
`).bind(...bindings).all();
|
|
160
168
|
|
|
161
|
-
const signals = (result.results || []).map(s => ({ ...s, reasons: tryParseJson(s.reasons, []) }));
|
|
169
|
+
const signals = (result.results || []).map((s: any) => ({ ...s, reasons: tryParseJson(s.reasons, []) }));
|
|
162
170
|
const stats = await env.DB.prepare(`SELECT * FROM v_fraud_dashboard`).first().catch(() => null);
|
|
163
171
|
|
|
164
172
|
return new Response(JSON.stringify({ success: true, period_hours: hours, total: signals.length, stats, alerts: signals }), { status: 200, headers });
|
|
165
|
-
} catch (err) {
|
|
166
|
-
console.error('[Fraud] alerts error:', err
|
|
167
|
-
return new Response(JSON.stringify({ error: err
|
|
173
|
+
} catch (err: any) {
|
|
174
|
+
console.error('[Fraud] alerts error:', err?.message || String(err));
|
|
175
|
+
return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
|
|
168
176
|
}
|
|
169
177
|
}
|
|
170
178
|
|
|
171
179
|
// ── GET /api/fraud/blocklist ──────────────────────────────────────────────────
|
|
172
|
-
export async function handleFraudBlocklist(env, request, headers) {
|
|
180
|
+
export async function handleFraudBlocklist(env: Env, request: Request, headers: Headers): Promise<Response> {
|
|
173
181
|
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
174
182
|
|
|
175
183
|
try {
|
|
@@ -179,19 +187,19 @@ export async function handleFraudBlocklist(env, request, headers) {
|
|
|
179
187
|
FROM fraud_alerts WHERE is_blocked = 1 ORDER BY events_dropped DESC LIMIT 100
|
|
180
188
|
`).all();
|
|
181
189
|
|
|
182
|
-
const blocklist = (result.results || []).map(r => ({ ...r, top_reasons: tryParseJson(r.top_reasons, []) }));
|
|
190
|
+
const blocklist = (result.results || []).map((r: any) => ({ ...r, top_reasons: tryParseJson(r.top_reasons, []) }));
|
|
183
191
|
return new Response(JSON.stringify({ success: true, total: blocklist.length, blocklist }), { status: 200, headers });
|
|
184
|
-
} catch (err) {
|
|
185
|
-
console.error('[Fraud] blocklist error:', err
|
|
186
|
-
return new Response(JSON.stringify({ error: err
|
|
192
|
+
} catch (err: any) {
|
|
193
|
+
console.error('[Fraud] blocklist error:', err?.message || String(err));
|
|
194
|
+
return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
|
|
187
195
|
}
|
|
188
196
|
}
|
|
189
197
|
|
|
190
198
|
// ── POST /api/fraud/blocklist/add ─────────────────────────────────────────────
|
|
191
|
-
export async function handleFraudBlocklistAdd(env, request, headers) {
|
|
199
|
+
export async function handleFraudBlocklistAdd(env: Env, request: Request, headers: Headers): Promise<Response> {
|
|
192
200
|
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
193
201
|
|
|
194
|
-
let body;
|
|
202
|
+
let body: any;
|
|
195
203
|
try { body = await request.json(); }
|
|
196
204
|
catch { return new Response(JSON.stringify({ error: 'JSON inválido' }), { status: 400, headers }); }
|
|
197
205
|
|
|
@@ -222,17 +230,17 @@ export async function handleFraudBlocklistAdd(env, request, headers) {
|
|
|
222
230
|
success: true, entity_type, entity_value, kv_key: kvKey, ttl_hours, expires_at: expiresAt,
|
|
223
231
|
message: `${entity_type} '${entity_value}' bloqueado por ${ttl_hours}h. Efeito imediato via KV.`,
|
|
224
232
|
}), { status: 200, headers });
|
|
225
|
-
} catch (err) {
|
|
226
|
-
console.error('[Fraud] blocklist add error:', err
|
|
227
|
-
return new Response(JSON.stringify({ error: err
|
|
233
|
+
} catch (err: any) {
|
|
234
|
+
console.error('[Fraud] blocklist add error:', err?.message || String(err));
|
|
235
|
+
return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
|
|
228
236
|
}
|
|
229
237
|
}
|
|
230
238
|
|
|
231
239
|
// ── DELETE /api/fraud/blocklist/remove ───────────────────────────────────────
|
|
232
|
-
export async function handleFraudBlocklistRemove(env, request, headers) {
|
|
240
|
+
export async function handleFraudBlocklistRemove(env: Env, request: Request, headers: Headers): Promise<Response> {
|
|
233
241
|
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
234
242
|
|
|
235
|
-
let body;
|
|
243
|
+
let body: any;
|
|
236
244
|
try { body = await request.json(); }
|
|
237
245
|
catch { return new Response(JSON.stringify({ error: 'JSON inválido' }), { status: 400, headers }); }
|
|
238
246
|
|
|
@@ -250,14 +258,14 @@ export async function handleFraudBlocklistRemove(env, request, headers) {
|
|
|
250
258
|
success: true, entity_type, entity_value,
|
|
251
259
|
message: `${entity_type} '${entity_value}' removido do blocklist. Efeito imediato via KV.`,
|
|
252
260
|
}), { status: 200, headers });
|
|
253
|
-
} catch (err) {
|
|
254
|
-
console.error('[Fraud] blocklist remove error:', err
|
|
255
|
-
return new Response(JSON.stringify({ error: err
|
|
261
|
+
} catch (err: any) {
|
|
262
|
+
console.error('[Fraud] blocklist remove error:', err?.message || String(err));
|
|
263
|
+
return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
|
|
256
264
|
}
|
|
257
265
|
}
|
|
258
266
|
|
|
259
267
|
// ── GET /api/fraud/stats ──────────────────────────────────────────────────────
|
|
260
|
-
export async function handleFraudStats(env, request, headers) {
|
|
268
|
+
export async function handleFraudStats(env: Env, request: Request, headers: Headers): Promise<Response> {
|
|
261
269
|
if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
|
|
262
270
|
|
|
263
271
|
try {
|
|
@@ -279,8 +287,8 @@ export async function handleFraudStats(env, request, headers) {
|
|
|
279
287
|
top_attacking_ips: topIps.results || [],
|
|
280
288
|
by_action: topReasons.results || [],
|
|
281
289
|
}), { status: 200, headers });
|
|
282
|
-
} catch (err) {
|
|
283
|
-
console.error('[Fraud] stats error:', err
|
|
284
|
-
return new Response(JSON.stringify({ error: err
|
|
290
|
+
} catch (err: any) {
|
|
291
|
+
console.error('[Fraud] stats error:', err?.message || String(err));
|
|
292
|
+
return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
|
|
285
293
|
}
|
|
286
294
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CDP Edge — Logistic Regression (pure
|
|
2
|
+
* CDP Edge — Logistic Regression (pure TS, sem deps externas)
|
|
3
3
|
* Treina modelo de predição de conversão com dados reais do D1.
|
|
4
4
|
*
|
|
5
5
|
* Features usadas (todas normalizadas 0-1):
|
|
@@ -7,9 +7,40 @@
|
|
|
7
7
|
* has_email, has_phone, is_br, hour_normalized
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { Env } from '../../types.js';
|
|
11
|
+
import { D1Database, KVNamespace } from '@cloudflare/workers-types';
|
|
12
|
+
|
|
13
|
+
// ── Tipos ───────────────────────────────────────────────────────────────────────
|
|
14
|
+
export interface DatasetRow {
|
|
15
|
+
features: number[];
|
|
16
|
+
label: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LogisticModel {
|
|
20
|
+
bias: number;
|
|
21
|
+
weights: number[];
|
|
22
|
+
accuracy: number;
|
|
23
|
+
positiveRate: number;
|
|
24
|
+
sampleSize: number;
|
|
25
|
+
threshold: number;
|
|
26
|
+
featureNames: string[];
|
|
27
|
+
trainedAt: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ExtractedFeatures {
|
|
31
|
+
utm_score: number;
|
|
32
|
+
engagement: number;
|
|
33
|
+
intention: number;
|
|
34
|
+
recency: number;
|
|
35
|
+
has_email: number;
|
|
36
|
+
has_phone: number;
|
|
37
|
+
is_br: number;
|
|
38
|
+
hour: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
10
41
|
// ── Feature Engineering ───────────────────────────────────────────────────────
|
|
11
42
|
|
|
12
|
-
const UTM_SCORES = {
|
|
43
|
+
const UTM_SCORES: Record<string, number> = {
|
|
13
44
|
facebook: 0.90, instagram: 0.90, meta: 0.90,
|
|
14
45
|
google: 0.82, youtube: 0.82,
|
|
15
46
|
tiktok: 0.75,
|
|
@@ -18,14 +49,14 @@ const UTM_SCORES = {
|
|
|
18
49
|
direct: 0.20,
|
|
19
50
|
};
|
|
20
51
|
|
|
21
|
-
const INTENTION_SCORES = {
|
|
52
|
+
const INTENTION_SCORES: Record<string, number> = {
|
|
22
53
|
comprador: 1.00, high_intent: 1.00,
|
|
23
54
|
interessado: 0.60,
|
|
24
55
|
nurture: 0.30,
|
|
25
56
|
curioso: 0.15,
|
|
26
57
|
};
|
|
27
58
|
|
|
28
|
-
export function extractFeatures(row) {
|
|
59
|
+
export function extractFeatures(row: any): number[] {
|
|
29
60
|
const src = (row.utm_source || '').toLowerCase().trim();
|
|
30
61
|
const intention = (row.intention_level || '').toLowerCase().trim();
|
|
31
62
|
const daysSince = row.days_since_lead || 0;
|
|
@@ -44,13 +75,13 @@ export function extractFeatures(row) {
|
|
|
44
75
|
|
|
45
76
|
// ── Sigmoid ───────────────────────────────────────────────────────────────────
|
|
46
77
|
|
|
47
|
-
function sigmoid(z) {
|
|
78
|
+
function sigmoid(z: number): number {
|
|
48
79
|
if (z > 20) return 1;
|
|
49
80
|
if (z < -20) return 0;
|
|
50
81
|
return 1 / (1 + Math.exp(-z));
|
|
51
82
|
}
|
|
52
83
|
|
|
53
|
-
function dot(weights, features) {
|
|
84
|
+
function dot(weights: number[], features: number[]): number {
|
|
54
85
|
return features.reduce((sum, f, i) => sum + (weights[i] || 0) * f, 0);
|
|
55
86
|
}
|
|
56
87
|
|
|
@@ -58,11 +89,8 @@ function dot(weights, features) {
|
|
|
58
89
|
|
|
59
90
|
/**
|
|
60
91
|
* Treina regressão logística com gradiente descendente.
|
|
61
|
-
* @param {Array<{features: number[], label: number}>} dataset
|
|
62
|
-
* @param {{ iterations?, learningRate?, lambda? }} opts
|
|
63
|
-
* @returns {{ bias, weights, accuracy, positiveRate }}
|
|
64
92
|
*/
|
|
65
|
-
export function trainLogisticRegression(dataset, opts = {}) {
|
|
93
|
+
export function trainLogisticRegression(dataset: DatasetRow[], opts: { iterations?: number; learningRate?: number; lambda?: number } = {}): LogisticModel | null {
|
|
66
94
|
if (!dataset || dataset.length < 50) {
|
|
67
95
|
return null; // dados insuficientes
|
|
68
96
|
}
|
|
@@ -132,11 +160,8 @@ export function trainLogisticRegression(dataset, opts = {}) {
|
|
|
132
160
|
|
|
133
161
|
/**
|
|
134
162
|
* Prediz score de conversão (0-100) usando pesos treinados.
|
|
135
|
-
* @param {{ bias, weights, threshold }} model
|
|
136
|
-
* @param {number[]} features
|
|
137
|
-
* @returns {number} score 0-100
|
|
138
163
|
*/
|
|
139
|
-
export function predictWithWeights(model, features) {
|
|
164
|
+
export function predictWithWeights(model: LogisticModel, features: number[]): number {
|
|
140
165
|
const z = dot(model.weights, features) + model.bias;
|
|
141
166
|
const prob = sigmoid(z);
|
|
142
167
|
return Math.round(prob * 100);
|
|
@@ -146,11 +171,11 @@ export function predictWithWeights(model, features) {
|
|
|
146
171
|
|
|
147
172
|
export const LTV_WEIGHTS_KV_KEY = 'ltv_weights_active';
|
|
148
173
|
|
|
149
|
-
export async function loadActiveWeights(env) {
|
|
174
|
+
export async function loadActiveWeights(env: Env): Promise<LogisticModel | null> {
|
|
150
175
|
// 1. Tentar KV (cache ~7 dias)
|
|
151
176
|
if (env.GEO_CACHE) {
|
|
152
177
|
try {
|
|
153
|
-
const cached = await env.GEO_CACHE.get(LTV_WEIGHTS_KV_KEY, 'json');
|
|
178
|
+
const cached = await env.GEO_CACHE.get(LTV_WEIGHTS_KV_KEY, 'json') as LogisticModel | null;
|
|
154
179
|
if (cached?.weights?.length) return cached;
|
|
155
180
|
} catch {}
|
|
156
181
|
}
|
|
@@ -161,8 +186,8 @@ export async function loadActiveWeights(env) {
|
|
|
161
186
|
const row = await env.DB.prepare(
|
|
162
187
|
`SELECT weights_json FROM ltv_model_weights WHERE is_active = 1 ORDER BY trained_at DESC LIMIT 1`
|
|
163
188
|
).first();
|
|
164
|
-
if (!row
|
|
165
|
-
const model = JSON.parse(row.weights_json);
|
|
189
|
+
if (!row || !(row as any).weights_json) return null;
|
|
190
|
+
const model = JSON.parse((row as any).weights_json) as LogisticModel;
|
|
166
191
|
|
|
167
192
|
// Popular KV para próximas requests
|
|
168
193
|
if (env.GEO_CACHE && model?.weights?.length) {
|
|
@@ -174,7 +199,7 @@ export async function loadActiveWeights(env) {
|
|
|
174
199
|
}
|
|
175
200
|
}
|
|
176
201
|
|
|
177
|
-
export async function saveWeights(DB, model) {
|
|
202
|
+
export async function saveWeights(DB: D1Database, model: LogisticModel): Promise<void> {
|
|
178
203
|
if (!DB || !model) return;
|
|
179
204
|
const now = new Date().toISOString();
|
|
180
205
|
|