cdp-edge 1.0.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.
Files changed (176) hide show
  1. package/README.md +324 -0
  2. package/bin/cdp-edge.js +71 -0
  3. package/contracts/agent-versions.json +679 -0
  4. package/contracts/api-versions.json +372 -0
  5. package/contracts/types.ts +81 -0
  6. package/dist/commands/analyze.js +52 -0
  7. package/dist/commands/infra.js +54 -0
  8. package/dist/commands/install.js +191 -0
  9. package/dist/commands/server.js +174 -0
  10. package/dist/commands/setup.js +355 -0
  11. package/dist/commands/validate.js +248 -0
  12. package/dist/index.js +12 -0
  13. package/dist/sdk/cdpTrack.js +2095 -0
  14. package/dist/sdk/cdpTrack.min.js +64 -0
  15. package/dist/sdk/install-snippet.html +10 -0
  16. package/docs/CI-CD-SETUP.md +217 -0
  17. package/docs/events-reference.md +359 -0
  18. package/docs/installation.md +155 -0
  19. package/docs/quick-start.md +185 -0
  20. package/docs/sdk-reference.md +371 -0
  21. package/docs/whatsapp-ctwa.md +210 -0
  22. package/extracted-skill/tracking-events-generator/INDEX.md +94 -0
  23. package/extracted-skill/tracking-events-generator/INSTALACAO-CDPEDGE.md +58 -0
  24. package/extracted-skill/tracking-events-generator/INTEGRACAO-COMPLETA.md +683 -0
  25. package/extracted-skill/tracking-events-generator/MELHORIAS-IMPLEMENTADAS.md +513 -0
  26. package/extracted-skill/tracking-events-generator/Premium-Tracking-Intelligence-Resumo.md +333 -0
  27. package/extracted-skill/tracking-events-generator/SKILL.md +257 -0
  28. package/extracted-skill/tracking-events-generator/advanced-matching.js +364 -0
  29. package/extracted-skill/tracking-events-generator/agents/ab-ltv-agent.md +196 -0
  30. package/extracted-skill/tracking-events-generator/agents/ab-testing-agent.md +54 -0
  31. package/extracted-skill/tracking-events-generator/agents/attribution-agent.md +1304 -0
  32. package/extracted-skill/tracking-events-generator/agents/bidding-agent.md +347 -0
  33. package/extracted-skill/tracking-events-generator/agents/bing-agent.md +66 -0
  34. package/extracted-skill/tracking-events-generator/agents/browser-tracking.md +364 -0
  35. package/extracted-skill/tracking-events-generator/agents/code-guardian-agent.md +149 -0
  36. package/extracted-skill/tracking-events-generator/agents/compliance-agent.md +2097 -0
  37. package/extracted-skill/tracking-events-generator/agents/crm-integration-agent.md +1459 -0
  38. package/extracted-skill/tracking-events-generator/agents/dashboard-agent.md +456 -0
  39. package/extracted-skill/tracking-events-generator/agents/database-agent.md +668 -0
  40. package/extracted-skill/tracking-events-generator/agents/debug-agent.md +1455 -0
  41. package/extracted-skill/tracking-events-generator/agents/devops-agent.md +232 -0
  42. package/extracted-skill/tracking-events-generator/agents/domain-setup-agent.md +238 -0
  43. package/extracted-skill/tracking-events-generator/agents/email-agent.md +88 -0
  44. package/extracted-skill/tracking-events-generator/agents/fingerprint-agent.md +257 -0
  45. package/extracted-skill/tracking-events-generator/agents/fraud-detection-agent.md +143 -0
  46. package/extracted-skill/tracking-events-generator/agents/google-agent.md +235 -0
  47. package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +525 -0
  48. package/extracted-skill/tracking-events-generator/agents/lead-scoring-agent.md +282 -0
  49. package/extracted-skill/tracking-events-generator/agents/linkedin-agent.md +173 -0
  50. package/extracted-skill/tracking-events-generator/agents/localization-agent.md +55 -0
  51. package/extracted-skill/tracking-events-generator/agents/ltv-predictor-agent.md +59 -0
  52. package/extracted-skill/tracking-events-generator/agents/master-feedback-loop.md +960 -0
  53. package/extracted-skill/tracking-events-generator/agents/master-orchestrator.md +2154 -0
  54. package/extracted-skill/tracking-events-generator/agents/match-quality-agent.md +304 -0
  55. package/extracted-skill/tracking-events-generator/agents/memory-agent.json +25 -0
  56. package/extracted-skill/tracking-events-generator/agents/memory-agent.md +878 -0
  57. package/extracted-skill/tracking-events-generator/agents/meta-agent.md +118 -0
  58. package/extracted-skill/tracking-events-generator/agents/ml-clustering-agent.md +749 -0
  59. package/extracted-skill/tracking-events-generator/agents/page-analyzer.md +272 -0
  60. package/extracted-skill/tracking-events-generator/agents/performance-agent.md +1167 -0
  61. package/extracted-skill/tracking-events-generator/agents/performance-optimization-agent.md +1442 -0
  62. package/extracted-skill/tracking-events-generator/agents/pinterest-agent.md +318 -0
  63. package/extracted-skill/tracking-events-generator/agents/premium-tracking-intelligence-agent.md +849 -0
  64. package/extracted-skill/tracking-events-generator/agents/r2-setup-agent.md +258 -0
  65. package/extracted-skill/tracking-events-generator/agents/reddit-agent.md +321 -0
  66. package/extracted-skill/tracking-events-generator/agents/security-enterprise-agent.md +1861 -0
  67. package/extracted-skill/tracking-events-generator/agents/server-tracking.md +1188 -0
  68. package/extracted-skill/tracking-events-generator/agents/spotify-agent.md +391 -0
  69. package/extracted-skill/tracking-events-generator/agents/tiktok-agent.md +182 -0
  70. package/extracted-skill/tracking-events-generator/agents/tracking-plan-agent.md +459 -0
  71. package/extracted-skill/tracking-events-generator/agents/utm-agent.md +322 -0
  72. package/extracted-skill/tracking-events-generator/agents/validator-agent.md +271 -0
  73. package/extracted-skill/tracking-events-generator/agents/webhook-agent.md +177 -0
  74. package/extracted-skill/tracking-events-generator/agents/whatsapp-agent.md +129 -0
  75. package/extracted-skill/tracking-events-generator/agents/whatsapp-ctwa-setup-agent.md +707 -0
  76. package/extracted-skill/tracking-events-generator/agents/youtube-agent.md +537 -0
  77. package/extracted-skill/tracking-events-generator/anti-blocking.js +285 -0
  78. package/extracted-skill/tracking-events-generator/cdpTrack.js +640 -0
  79. package/extracted-skill/tracking-events-generator/contracts/api-versions.json +372 -0
  80. package/extracted-skill/tracking-events-generator/docs/guia-cloudflare-iniciante.md +107 -0
  81. package/extracted-skill/tracking-events-generator/engagement-scoring.js +226 -0
  82. package/extracted-skill/tracking-events-generator/evals/evals.json +235 -0
  83. package/extracted-skill/tracking-events-generator/integration-test.js +497 -0
  84. package/extracted-skill/tracking-events-generator/knowledge-base.md +3066 -0
  85. package/extracted-skill/tracking-events-generator/micro-events.js +992 -0
  86. package/extracted-skill/tracking-events-generator/models/captura-de-lead.md +78 -0
  87. package/extracted-skill/tracking-events-generator/models/captura-lead-evento-externo.md +99 -0
  88. package/extracted-skill/tracking-events-generator/models/checkout-proprio.md +111 -0
  89. package/extracted-skill/tracking-events-generator/models/lancamento-imobiliario.md +344 -0
  90. package/extracted-skill/tracking-events-generator/models/multi-step-checkout.md +672 -0
  91. package/extracted-skill/tracking-events-generator/models/pagina-obrigado.md +55 -0
  92. package/extracted-skill/tracking-events-generator/models/pinterest/conversions-api-template.js +144 -0
  93. package/extracted-skill/tracking-events-generator/models/pinterest/event-mappings.json +48 -0
  94. package/extracted-skill/tracking-events-generator/models/pinterest/tag-template.js +28 -0
  95. package/extracted-skill/tracking-events-generator/models/quiz-funnel.md +132 -0
  96. package/extracted-skill/tracking-events-generator/models/reddit/conversions-api-template.js +205 -0
  97. package/extracted-skill/tracking-events-generator/models/reddit/event-mappings.json +56 -0
  98. package/extracted-skill/tracking-events-generator/models/reddit/pixel-template.js +19 -0
  99. package/extracted-skill/tracking-events-generator/models/scenarios/behavior-engine.js +425 -0
  100. package/extracted-skill/tracking-events-generator/models/scenarios/real-estate-logic.md +50 -0
  101. package/extracted-skill/tracking-events-generator/models/scenarios/sales-page-logic.md +50 -0
  102. package/extracted-skill/tracking-events-generator/models/trafego-direto.md +582 -0
  103. package/extracted-skill/tracking-events-generator/models/webinar-registration.md +63 -0
  104. package/extracted-skill/tracking-events-generator/route-intent-capture.js +222 -0
  105. package/extracted-skill/tracking-events-generator/tracking.config.js +46 -0
  106. package/extracted-skill/tracking-events-generator/walkthrough.md +26 -0
  107. package/package.json +89 -0
  108. package/scripts/build-sdk.js +106 -0
  109. package/server-edge-tracker/.client.env.example +14 -0
  110. package/server-edge-tracker/INSTALAR.md +527 -0
  111. package/server-edge-tracker/SEGMENTATION-DOCS.md +513 -0
  112. package/server-edge-tracker/config/utm-mapping.json +64 -0
  113. package/server-edge-tracker/deploy-client.cjs +76 -0
  114. package/server-edge-tracker/index.ts +1164 -0
  115. package/server-edge-tracker/migrate-new-db.sql +137 -0
  116. package/server-edge-tracker/migrate-v2.sql +16 -0
  117. package/server-edge-tracker/migrate-v3.sql +6 -0
  118. package/server-edge-tracker/migrate-v4.sql +18 -0
  119. package/server-edge-tracker/migrate-v5.sql +17 -0
  120. package/server-edge-tracker/migrate-v6.sql +24 -0
  121. package/server-edge-tracker/migrate-v7.sql +64 -0
  122. package/server-edge-tracker/migrate.sql +111 -0
  123. package/server-edge-tracker/modules/db.ts +702 -0
  124. package/server-edge-tracker/modules/dispatch/ga4.ts +72 -0
  125. package/server-edge-tracker/modules/dispatch/meta.ts +143 -0
  126. package/server-edge-tracker/modules/dispatch/platforms.ts +255 -0
  127. package/server-edge-tracker/modules/dispatch/tiktok.ts +107 -0
  128. package/server-edge-tracker/modules/dispatch/whatsapp.ts +279 -0
  129. package/server-edge-tracker/modules/intelligence.ts +589 -0
  130. package/server-edge-tracker/modules/ml/bidding.ts +247 -0
  131. package/server-edge-tracker/modules/ml/fraud.ts +302 -0
  132. package/server-edge-tracker/modules/ml/logistic.ts +226 -0
  133. package/server-edge-tracker/modules/ml/ltv.ts +531 -0
  134. package/server-edge-tracker/modules/ml/matchquality.ts +232 -0
  135. package/server-edge-tracker/modules/ml/quiz.ts +343 -0
  136. package/server-edge-tracker/modules/ml/roas.ts +255 -0
  137. package/server-edge-tracker/modules/ml/segmentation.ts +407 -0
  138. package/server-edge-tracker/modules/nurture.ts +257 -0
  139. package/server-edge-tracker/modules/utils.ts +311 -0
  140. package/server-edge-tracker/modules/utm/utm-enricher.ts +231 -0
  141. package/server-edge-tracker/schema-ab-ltv.sql +97 -0
  142. package/server-edge-tracker/schema-bidding.sql +86 -0
  143. package/server-edge-tracker/schema-fraud.sql +90 -0
  144. package/server-edge-tracker/schema-indexes.sql +67 -0
  145. package/server-edge-tracker/schema-ltv-feedback.sql +11 -0
  146. package/server-edge-tracker/schema-quiz.sql +52 -0
  147. package/server-edge-tracker/schema-sales-engine.sql +113 -0
  148. package/server-edge-tracker/schema-segmentation.sql +219 -0
  149. package/server-edge-tracker/schema-utm.sql +82 -0
  150. package/server-edge-tracker/schema.sql +265 -0
  151. package/server-edge-tracker/types.ts +258 -0
  152. package/server-edge-tracker/wrangler.toml +136 -0
  153. package/templates/afiliado-sem-landing.md +312 -0
  154. package/templates/captura-de-lead.md +78 -0
  155. package/templates/captura-lead-evento-externo.md +99 -0
  156. package/templates/checkout-proprio.md +111 -0
  157. package/templates/install/.claude/commands/cdp.md +1 -0
  158. package/templates/install/CLAUDE.md +65 -0
  159. package/templates/lancamento-imobiliario.md +344 -0
  160. package/templates/linkedin/tag-template.js +46 -0
  161. package/templates/multi-step-checkout.md +672 -0
  162. package/templates/pagina-obrigado.md +55 -0
  163. package/templates/pinterest/conversions-api-template.js +144 -0
  164. package/templates/pinterest/event-mappings.json +48 -0
  165. package/templates/pinterest/tag-template.js +28 -0
  166. package/templates/quiz-funnel.md +132 -0
  167. package/templates/reddit/conversions-api-template.js +205 -0
  168. package/templates/reddit/event-mappings.json +56 -0
  169. package/templates/reddit/pixel-template.js +19 -0
  170. package/templates/scenarios/behavior-engine.js +425 -0
  171. package/templates/scenarios/real-estate-logic.md +50 -0
  172. package/templates/scenarios/sales-page-logic.md +50 -0
  173. package/templates/spotify/pixel-template.js +46 -0
  174. package/templates/trafego-direto.md +582 -0
  175. package/templates/vsl-page.md +292 -0
  176. package/templates/webinar-registration.md +63 -0
@@ -0,0 +1,702 @@
1
+ /**
2
+ * CDP Edge — Camada D1 (Database)
3
+ * Todas as operações de escrita/leitura no banco D1.
4
+ * Bindings: env.DB, env.GEO_CACHE, env.AUDIT_LOGS
5
+ */
6
+
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
+ }
48
+
49
+ // ── saveLead — inserir evento de conversão ────────────────────────────────────
50
+ export async function saveLead(env: Env, eventName: string, payload: TrackPayload, request: Request, platform: string = 'website'): Promise<void> {
51
+ if (!env.DB) return;
52
+ try {
53
+ const {
54
+ email, phone, firstName, lastName,
55
+ city, state, country,
56
+ fbp, fbc, userId,
57
+ utmSource, utmMedium, utmCampaign, utmContent, utmTerm,
58
+ pageUrl, value, currency, eventId, botScore,
59
+ engagementScore, intentionLevel, utmRestored,
60
+ } = payload;
61
+
62
+ await env.DB.prepare(`
63
+ INSERT INTO leads (
64
+ event_name, event_id, email, phone, first_name, last_name,
65
+ city, state, country, fbp, fbc, user_id,
66
+ utm_source, utm_medium, utm_campaign, utm_content, utm_term,
67
+ page_url, value, currency, ip_address, platform, bot_score,
68
+ engagement_score, intention_level, utm_restored, created_at
69
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now'))
70
+ `).bind(
71
+ eventName,
72
+ eventId || null,
73
+ email || null,
74
+ normalizePhone(phone) || null,
75
+ firstName || null,
76
+ lastName || null,
77
+ city || null,
78
+ state || null,
79
+ (country || (request as any).cf?.country || null),
80
+ fbp || null,
81
+ fbc || null,
82
+ userId || null,
83
+ utmSource || null,
84
+ utmMedium || null,
85
+ utmCampaign || null,
86
+ utmContent || null,
87
+ utmTerm || null,
88
+ pageUrl || null,
89
+ value !== undefined ? parseFloat(String(value)) : null,
90
+ currency || 'BRL',
91
+ request.headers.get('CF-Connecting-IP') || null,
92
+ platform,
93
+ botScore || 0,
94
+ engagementScore !== undefined ? parseFloat(String(engagementScore)) : null,
95
+ intentionLevel || null,
96
+ utmRestored ? 1 : 0,
97
+ ).run();
98
+ } catch (err: any) {
99
+ console.error('D1 saveLead error:', err?.message || String(err));
100
+ }
101
+ }
102
+
103
+ // ── calculateCohortLabel ──────────────────────────────────────────────────────
104
+ export function calculateCohortLabel(score: number, eventName: string): string {
105
+ if (eventName === 'Purchase') return 'buyer_lookalike';
106
+ if (score >= 80) return 'high_intent';
107
+ if (score >= 30) return 'nurture';
108
+ return 'lost';
109
+ }
110
+
111
+ // ── upsertProfile — acumula cookies/scores entre visitas ─────────────────────
112
+ export async function upsertProfile(env: Env, eventName: string, payload: TrackPayload, request: Request): Promise<void> {
113
+ if (!env.DB || !payload.userId) return;
114
+ try {
115
+ const {
116
+ userId, email, phone,
117
+ fbp, fbc, ttp, gclid, ttclid, gaClientId,
118
+ city, state, country,
119
+ engagementScore, userScore,
120
+ } = payload;
121
+
122
+ const scoreMap: Record<string, number> = { PageView: 5, ViewContent: 10, ScrollDepth: 3, TimeOnPage: 5, Lead: 30, InitiateCheckout: 50, Purchase: 100 };
123
+ const eventScore = scoreMap[eventName] || 2;
124
+
125
+ const behaviorBonus = userScore
126
+ ? Math.round((Math.min(userScore, 100) / 100) * 20)
127
+ : (engagementScore ? Math.round((Math.min(engagementScore, 5) / 5) * 10) : 0);
128
+
129
+ const totalDelta = eventScore + behaviorBonus;
130
+
131
+ await env.DB.prepare(`
132
+ INSERT INTO user_profiles
133
+ (user_id, email, phone, fbp, fbc, ttp, gclid, ttclid, ga_client_id,
134
+ city, state, country, score, cohort_label, created_at, updated_at)
135
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now'),datetime('now'))
136
+ ON CONFLICT(user_id) DO UPDATE SET
137
+ email = COALESCE(excluded.email, user_profiles.email),
138
+ phone = COALESCE(excluded.phone, user_profiles.phone),
139
+ fbp = COALESCE(excluded.fbp, user_profiles.fbp),
140
+ fbc = COALESCE(excluded.fbc, user_profiles.fbc),
141
+ ttp = COALESCE(excluded.ttp, user_profiles.ttp),
142
+ gclid = COALESCE(excluded.gclid, user_profiles.gclid),
143
+ ttclid = COALESCE(excluded.ttclid, user_profiles.ttclid),
144
+ ga_client_id = COALESCE(excluded.ga_client_id, user_profiles.ga_client_id),
145
+ city = COALESCE(excluded.city, user_profiles.city),
146
+ state = COALESCE(excluded.state, user_profiles.state),
147
+ country = COALESCE(excluded.country, user_profiles.country),
148
+ score = user_profiles.score + excluded.score,
149
+ cohort_label = excluded.cohort_label,
150
+ updated_at = datetime('now')
151
+ `).bind(
152
+ userId,
153
+ email || null,
154
+ normalizePhone(phone) || null,
155
+ fbp || null,
156
+ fbc || null,
157
+ ttp || null,
158
+ gclid || null,
159
+ ttclid || null,
160
+ gaClientId || null,
161
+ city || null,
162
+ state || null,
163
+ (country || (request as any).cf?.country || null),
164
+ totalDelta,
165
+ calculateCohortLabel(totalDelta, eventName),
166
+ ).run();
167
+ } catch (err: any) {
168
+ console.error('D1 upsertProfile error:', err?.message || String(err));
169
+ }
170
+ }
171
+
172
+ // ── resolveDeviceGraph — Cross-Device Identity ────────────────────────────────
173
+ export async function resolveDeviceGraph(DB: D1Database, currentUserId: string, email?: string | null, phone?: string | null): Promise<void> {
174
+ if (!DB || !currentUserId) return;
175
+ if (!email && !phone) return;
176
+
177
+ try {
178
+ const conditions: string[] = [];
179
+ const bindings: (string | number)[] = [];
180
+
181
+ if (email) {
182
+ conditions.push('email = ?');
183
+ bindings.push(email.toLowerCase().trim());
184
+ }
185
+ if (phone) {
186
+ const digits = String(phone).replace(/\D/g, '');
187
+ if (digits.length >= 10) {
188
+ conditions.push('phone LIKE ?');
189
+ bindings.push(`%${digits.slice(-10)}`);
190
+ }
191
+ }
192
+
193
+ if (conditions.length === 0) return;
194
+
195
+ bindings.push(currentUserId);
196
+ const rows = await DB.prepare(`
197
+ SELECT user_id, email, phone, created_at
198
+ FROM user_profiles
199
+ WHERE (${conditions.join(' OR ')})
200
+ AND user_id != ?
201
+ ORDER BY created_at ASC
202
+ LIMIT 5
203
+ `).bind(...bindings).all();
204
+
205
+ if (!rows.results || rows.results.length === 0) return;
206
+
207
+ for (const match of rows.results) {
208
+ const emailMatch = email && match.email &&
209
+ email.toLowerCase().trim() === (match.email as string).toLowerCase().trim();
210
+ const phoneMatch = phone && match.phone && (() => {
211
+ const a = String(phone).replace(/\D/g, '');
212
+ const b = String(match.phone).replace(/\D/g, '');
213
+ return a.slice(-10) === b.slice(-10) && a.length >= 10;
214
+ })();
215
+
216
+ if (!emailMatch && !phoneMatch) continue;
217
+
218
+ const matchType = emailMatch && phoneMatch ? 'email+phone' : (emailMatch ? 'email' : 'phone');
219
+ const matchConfidence = emailMatch && phoneMatch ? 0.99 : (emailMatch ? 0.95 : 0.85);
220
+ const primary = match.user_id as string;
221
+ const secondary = currentUserId;
222
+
223
+ await DB.prepare(`
224
+ INSERT OR IGNORE INTO device_graph
225
+ (primary_user_id, secondary_user_id, match_type, match_confidence)
226
+ VALUES (?, ?, ?, ?)
227
+ `).bind(primary, secondary, matchType, matchConfidence).run();
228
+
229
+ // sem log de user IDs — dados sensíveis não entram em Workers log
230
+ }
231
+ } catch (err: any) {
232
+ console.error('resolveDeviceGraph error:', err?.message || String(err));
233
+ }
234
+ }
235
+
236
+ // ── fireAutomation — dispara regras de automação (WA/Email) ──────────────────
237
+ export async function fireAutomation(env: Env, eventName: string, leadId: number | null, payload: TrackPayload): Promise<void> {
238
+ if (!env.DB) return;
239
+
240
+ try {
241
+ const { results: rules } = await env.DB
242
+ .prepare(
243
+ `SELECT id, channel, subject_template, message_template
244
+ FROM automation_rules
245
+ WHERE trigger_event = ?1 AND is_active = 1`
246
+ )
247
+ .bind(eventName)
248
+ .all();
249
+
250
+ if (!rules || rules.length === 0) return;
251
+
252
+ const vars: Record<string, string> = {
253
+ name: String(payload.firstName || (payload as any).name || ''),
254
+ email: String(payload.email || ''),
255
+ phone: String(payload.phone || ''),
256
+ campaign: String(payload.utmCampaign || payload.utm_campaign || ''),
257
+ intention: String(payload.intentionLevel || payload.intention_level || ''),
258
+ };
259
+
260
+ const interpolate = (tpl: string) =>
261
+ tpl.replace(/\{\{(\w+)\}\}/g, (_, k) => vars[k] ?? '');
262
+
263
+ for (const rule of rules) {
264
+ const message = interpolate(rule.message_template as string);
265
+ const subject = rule.subject_template ? interpolate(rule.subject_template as string) : null;
266
+
267
+ try {
268
+ if (rule.channel === 'whatsapp' && payload.phone && env.WHATSAPP_ACCESS_TOKEN && env.WHATSAPP_PHONE_NUMBER_ID) {
269
+ const digits = String(payload.phone).replace(/\D/g, '');
270
+ const e164 = digits.startsWith('55') ? `+${digits}` : `+55${digits}`;
271
+ const waRes = await fetch(
272
+ `https://graph.facebook.com/v22.0/${env.WHATSAPP_PHONE_NUMBER_ID}/messages`,
273
+ {
274
+ method: 'POST',
275
+ headers: { 'Authorization': `Bearer ${env.WHATSAPP_ACCESS_TOKEN}`, 'Content-Type': 'application/json' },
276
+ body: JSON.stringify({ messaging_product: 'whatsapp', recipient_type: 'individual', to: e164, type: 'text', text: { body: message } }),
277
+ }
278
+ );
279
+ const waData = await waRes.json();
280
+ const status = waRes.ok ? 'sent' : 'failed';
281
+ const meta = waRes.ok ? ((waData as any).messages?.[0]?.id ?? null) : JSON.stringify(waData);
282
+ await env.DB.prepare(
283
+ `INSERT INTO messaging_history (lead_id, channel, recipient, subject, content, status, meta) VALUES (?1,?2,?3,?4,?5,?6,?7)`
284
+ ).bind(leadId, 'whatsapp', e164, null, message, status, meta).run();
285
+
286
+ } else if (rule.channel === 'email' && payload.email && env.RESEND_API_KEY) {
287
+ const resendRes = await fetch('https://api.resend.com/emails', {
288
+ method: 'POST',
289
+ headers: { 'Authorization': `Bearer ${env.RESEND_API_KEY}`, 'Content-Type': 'application/json' },
290
+ body: JSON.stringify({
291
+ from: env.RESEND_FROM_EMAIL || 'noreply@cdp-edge.app',
292
+ to: [payload.email],
293
+ subject: subject || `Olá, ${vars.name || 'você'}!`,
294
+ html: `<p>${message.replace(/\n/g, '<br>')}</p>`,
295
+ }),
296
+ });
297
+ const resendData = await resendRes.json();
298
+ const status = resendRes.ok ? 'sent' : 'failed';
299
+ const meta = resendRes.ok ? ((resendData as any).id ?? null) : JSON.stringify(resendData);
300
+ await env.DB.prepare(
301
+ `INSERT INTO messaging_history (lead_id, channel, recipient, subject, content, status, meta) VALUES (?1,?2,?3,?4,?5,?6,?7)`
302
+ ).bind(leadId, 'email', payload.email, subject, message, status, meta).run();
303
+ }
304
+ } catch (err: any) {
305
+ console.error(`[Automation] rule ${(rule as any).id} error:`, err?.message || String(err));
306
+ }
307
+ }
308
+ } catch (err: any) {
309
+ console.error('[Automation] fireAutomation error:', err?.message || String(err));
310
+ }
311
+ }
312
+
313
+ // ── getProfileByEmail ─────────────────────────────────────────────────────────
314
+ export async function getProfileByEmail(env: Env, email: string): Promise<any | null> {
315
+ if (!env.DB || !email) return null;
316
+ try {
317
+ return await env.DB.prepare(
318
+ 'SELECT * FROM user_profiles WHERE email = ? ORDER BY updated_at DESC LIMIT 1'
319
+ ).bind(email.toLowerCase().trim()).first();
320
+ } catch {
321
+ return null;
322
+ }
323
+ }
324
+
325
+ // ── enrichGeoFromEdge — enriquece payload com dados de geolocalização ─────────
326
+ export async function enrichGeoFromEdge(request: Request, env: Env, payload: TrackPayload): Promise<GeoData> {
327
+ const cf = (request as any).cf || {};
328
+ const ip = request.headers.get('CF-Connecting-IP') || '';
329
+
330
+ let geoData: GeoData | null = null;
331
+ if (env.GEO_CACHE && ip) {
332
+ try {
333
+ const cached = await env.GEO_CACHE.get(`geo:${ip}`, 'json') as GeoData | null;
334
+ if (cached) geoData = cached;
335
+ } catch (err: any) {
336
+ console.error('[DB] Error fetching geo data from cache:', {
337
+ ip,
338
+ error: err?.message || String(err),
339
+ stack: err?.stack,
340
+ });
341
+ }
342
+ }
343
+
344
+ if (!geoData) {
345
+ geoData = {
346
+ country: cf.country || null,
347
+ continent: cf.continent || null,
348
+ asn: cf.asn || null,
349
+ asOrg: cf.asOrganization || null,
350
+ colo: cf.colo || null,
351
+ city: cf.city || null,
352
+ region: cf.region || null,
353
+ regionCode: cf.regionCode || null,
354
+ postalCode: cf.postalCode || null,
355
+ latitude: cf.latitude || null,
356
+ longitude: cf.longitude || null,
357
+ timezone: cf.timezone || null,
358
+ metroCode: cf.metroCode || null,
359
+ };
360
+
361
+ if (env.GEO_CACHE && ip && geoData.country) {
362
+ try {
363
+ await env.GEO_CACHE.put(`geo:${ip}`, JSON.stringify(geoData), { expirationTtl: 3600 });
364
+ } catch (err: any) {
365
+ console.error('[DB] Error caching geo data:', {
366
+ ip,
367
+ country: geoData.country,
368
+ error: err?.message || String(err),
369
+ stack: err?.stack,
370
+ });
371
+ }
372
+ }
373
+ }
374
+
375
+ payload.country = payload.country || geoData.country;
376
+ payload.city = payload.city || geoData.city;
377
+ payload.state = payload.state || geoData.regionCode;
378
+ payload.zip = payload.zip || geoData.postalCode;
379
+ (payload as any).geo = geoData;
380
+
381
+ return geoData;
382
+ }
383
+
384
+ // ── writeAuditLog — grava evento no R2 ───────────────────────────────────────
385
+ export async function writeAuditLog(env: Env, eventName: string, payload: TrackPayload, geoData: GeoData | null): Promise<void> {
386
+ if (!env.AUDIT_LOGS) return;
387
+ try {
388
+ const now = new Date();
389
+ const y = now.getUTCFullYear();
390
+ const m = String(now.getUTCMonth() + 1).padStart(2, '0');
391
+ const d = String(now.getUTCDate()).padStart(2, '0');
392
+ const key = `logs/${y}/${m}/${d}/${now.getTime()}_${eventName}.json`;
393
+
394
+ const log = {
395
+ timestamp: now.toISOString(),
396
+ event: eventName,
397
+ userId: payload.userId || null,
398
+ eventId: payload.eventId || null,
399
+ value: payload.value || null,
400
+ currency: payload.currency || null,
401
+ ltvClass: payload.ltvClass || null,
402
+ utm: {
403
+ source: payload.utmSource || null,
404
+ medium: payload.utmMedium || null,
405
+ campaign: payload.utmCampaign || null,
406
+ content: payload.utmContent || null,
407
+ term: payload.utmTerm || null,
408
+ restored: payload.utmRestored || false,
409
+ },
410
+ geo: geoData || null,
411
+ };
412
+
413
+ await env.AUDIT_LOGS.put(key, JSON.stringify(log), {
414
+ httpMetadata: { contentType: 'application/json' },
415
+ });
416
+ } catch (err: any) {
417
+ console.error('[R2 Audit] Error:', err?.message || String(err));
418
+ }
419
+ }
420
+
421
+ // ── generateEdgeFingerprint ───────────────────────────────────────────────────
422
+ export async function generateEdgeFingerprint(request: Request): Promise<string | undefined> {
423
+ const asn = String((request as any).cf?.asn || '0');
424
+ const lang = (request.headers.get('Accept-Language') || 'unknown').split(',')[0].trim();
425
+ const ua = request.headers.get('User-Agent') || '';
426
+
427
+ const uaBase = ua
428
+ .toLowerCase()
429
+ .replace(/[\d.]+/g, '')
430
+ .replace(/[^a-z\s]/g, ' ')
431
+ .split(' ')
432
+ .filter(w => w.length > 3)
433
+ .slice(0, 4)
434
+ .join(' ')
435
+ .trim();
436
+
437
+ const raw = `${asn}|${lang}|${uaBase}`;
438
+ return sha256(raw);
439
+ }
440
+
441
+ // ── saveEdgeFingerprint ───────────────────────────────────────────────────────
442
+ export async function saveEdgeFingerprint(DB: D1Database, fingerprint: string | undefined, userId: string | undefined, payload: TrackPayload): Promise<void> {
443
+ if (!DB || !fingerprint) return;
444
+ const { utmSource, utmMedium, utmCampaign, utmContent, utmTerm } = payload;
445
+ if (!utmSource) return;
446
+
447
+ try {
448
+ await DB.prepare(`
449
+ INSERT INTO edge_fingerprints (fingerprint, user_id, utm_source, utm_medium, utm_campaign, utm_content, utm_term)
450
+ VALUES (?, ?, ?, ?, ?, ?, ?)
451
+ `).bind(
452
+ fingerprint,
453
+ userId || null,
454
+ utmSource || null,
455
+ utmMedium || null,
456
+ utmCampaign || null,
457
+ utmContent || null,
458
+ utmTerm || null,
459
+ ).run();
460
+ } catch (err: any) {
461
+ console.error('saveEdgeFingerprint error:', err?.message || String(err));
462
+ }
463
+ }
464
+
465
+ // ── resurrectUTM ──────────────────────────────────────────────────────────────
466
+ export async function resurrectUTM(DB: D1Database, fingerprint: string | undefined): Promise<any | null> {
467
+ if (!DB || !fingerprint) return null;
468
+ try {
469
+ return await DB.prepare(`
470
+ SELECT utm_source, utm_medium, utm_campaign, utm_content, utm_term
471
+ FROM edge_fingerprints
472
+ WHERE fingerprint = ?
473
+ AND utm_source IS NOT NULL
474
+ AND created_at > datetime('now', '-48 hours')
475
+ ORDER BY created_at DESC
476
+ LIMIT 1
477
+ `).bind(fingerprint).first();
478
+ } catch {
479
+ return null;
480
+ }
481
+ }
482
+
483
+ // ── upsertLtvProfile — persiste LTV no perfil ────────────────────────────────
484
+ export async function upsertLtvProfile(env: Env, userId: string, ltv: LtvResult): Promise<void> {
485
+ if (!env.DB || !userId) return;
486
+ try {
487
+ await env.DB.prepare(`
488
+ UPDATE user_profiles
489
+ SET predicted_ltv_class = ?,
490
+ predicted_ltv_value = ?,
491
+ updated_at = datetime('now')
492
+ WHERE user_id = ?
493
+ `).bind(ltv.class, ltv.value, userId).run();
494
+ } catch (err: any) {
495
+ console.error('upsertLtvProfile error:', err?.message || String(err));
496
+ }
497
+ }
498
+
499
+ // ── recordLtvFeedback — fecha o ciclo preditivo com valor real de compra ─────
500
+ // Chamado em background quando um Purchase chega com payload.value > 0.
501
+ // Atualiza user_profiles + ltv_ab_assignments + ltv_ab_variations em cascata.
502
+ export async function recordLtvFeedback(env: Env, userId: string, realValue: number): Promise<void> {
503
+ if (!env.DB || !userId || !realValue || realValue <= 0) return;
504
+
505
+ try {
506
+ // 1. Busca predicted_ltv_value atual do perfil
507
+ const profile = await env.DB.prepare(`
508
+ SELECT predicted_ltv_value FROM user_profiles WHERE user_id = ?
509
+ `).bind(userId).first();
510
+
511
+ // accuracy = 1 - |pred-real|/real (0–1, mesmo padrão do A/B test accuracy_score)
512
+ const predictedValue = profile?.predicted_ltv_value;
513
+ const ltv_accuracy = (predictedValue !== null && predictedValue !== undefined)
514
+ ? Math.max(0, Math.round((1 - Math.abs(Number(predictedValue) - realValue) / Math.max(realValue, 1)) * 100) / 100)
515
+ : null;
516
+
517
+ // 2. Grava valor real + accuracy no perfil
518
+ await env.DB.prepare(`
519
+ UPDATE user_profiles
520
+ SET real_ltv_value = ?,
521
+ ltv_accuracy = ?,
522
+ ltv_feedback_at = datetime('now'),
523
+ updated_at = datetime('now')
524
+ WHERE user_id = ?
525
+ `).bind(realValue, ltv_accuracy, userId).run();
526
+
527
+ // 3. Fecha assignment do A/B test mais recente não convertido (janela 60 dias)
528
+ const assignment = await env.DB.prepare(`
529
+ SELECT id, variation_id, predicted_ltv
530
+ FROM ltv_ab_assignments
531
+ WHERE user_id = ?
532
+ AND converted = 0
533
+ AND assigned_at > datetime('now', '-60 days')
534
+ ORDER BY assigned_at DESC
535
+ LIMIT 1
536
+ `).bind(userId).first();
537
+
538
+ if (!assignment) return;
539
+
540
+ // 3a. Marca assignment como convertido
541
+ await env.DB.prepare(`
542
+ UPDATE ltv_ab_assignments
543
+ SET converted = 1,
544
+ real_revenue = ?,
545
+ converted_at = datetime('now')
546
+ WHERE id = ?
547
+ `).bind(realValue, (assignment as any).id).run();
548
+
549
+ // 3b. Atualiza métricas acumuladas da variação (running average — safe para concorrência D1)
550
+ const predLtv = (assignment as any).predicted_ltv || 0;
551
+ const indivAcc = Math.max(0, 1 - Math.abs(predLtv - realValue) / Math.max(realValue, 1));
552
+
553
+ await env.DB.prepare(`
554
+ UPDATE ltv_ab_variations
555
+ SET total_purchases = total_purchases + 1,
556
+ sum_real_revenue = sum_real_revenue + ?,
557
+ avg_real_revenue = (sum_real_revenue + ?) / (total_purchases + 1),
558
+ accuracy_score = ROUND(
559
+ (COALESCE(accuracy_score, 0) * total_purchases + ?) / (total_purchases + 1),
560
+ 4
561
+ )
562
+ WHERE id = ?
563
+ `).bind(realValue, realValue, indivAcc, (assignment as any).variation_id).run();
564
+
565
+ } catch (err: any) {
566
+ console.error('[LTV-Feedback] recordLtvFeedback error:', err?.message || String(err));
567
+ }
568
+ }
569
+
570
+ // ── Feedback Loop — Log de falhas e métricas de saúde ────────────────────────
571
+
572
+ export async function logApiFailure(DB: D1Database, platform: string, eventName: string, errorCode: string | number, errorMessage: string, eventId: string, rawPayload: string): Promise<void> {
573
+ try {
574
+ await DB.prepare(`
575
+ INSERT INTO api_failures (platform, event_name, error_code, error_message, event_id, raw_payload, retry_count, final_status)
576
+ VALUES (?, ?, ?, ?, ?, ?, 0, 'failed')
577
+ `).bind(platform, eventName, String(errorCode), errorMessage, eventId, rawPayload).run();
578
+ } catch (err: any) {
579
+ console.error('Failed to log API failure:', err?.message || String(err));
580
+ }
581
+ }
582
+
583
+ export async function getHealthMetrics(DB: D1Database, platform: string, hours: number = 24): Promise<HealthMetrics> {
584
+ try {
585
+ const failures = await DB.prepare(`
586
+ SELECT COUNT(*) as count, error_code
587
+ FROM api_failures
588
+ WHERE platform = ? AND created_at > datetime('now', '-${hours} hours')
589
+ GROUP BY error_code
590
+ `).bind(platform).all();
591
+
592
+ const totalSent = await DB.prepare(`
593
+ SELECT COUNT(*) as count
594
+ FROM leads
595
+ WHERE platform = ? AND created_at > datetime('now', '-${hours} hours')
596
+ `).bind(platform).first();
597
+
598
+ const totalFailed = failures.results?.reduce((sum: number, f: any) => sum + f.count, 0) || 0;
599
+ const successRate = (totalSent as any)?.count > 0
600
+ ? (((totalSent as any).count - totalFailed) / (totalSent as any).count) * 100
601
+ : 100;
602
+
603
+ return {
604
+ platform,
605
+ hours,
606
+ events_sent: (totalSent as any)?.count || 0,
607
+ events_failed: totalFailed,
608
+ success_rate: successRate,
609
+ errors_detected: (failures.results || []).map((f: any) => ({ code: f.error_code, count: f.count })),
610
+ issues: totalFailed > ((totalSent as any)?.count || 0) * 0.1 ? ['high_error_rate'] : [],
611
+ };
612
+ } catch (err: any) {
613
+ console.error('Failed to get health metrics:', err?.message || String(err));
614
+ return { platform, hours, events_sent: 0, events_failed: 0, success_rate: 0, errors_detected: [], issues: ['metrics_unavailable'] };
615
+ }
616
+ }
617
+
618
+ export async function generateDailyReport(DB: D1Database): Promise<DailyReport[]> {
619
+ const platforms = ['meta', 'ga4', 'tiktok', 'pinterest', 'reddit'];
620
+ const today = new Date().toISOString().split('T')[0];
621
+ const reports: DailyReport[] = [];
622
+
623
+ for (const platform of platforms) {
624
+ const metrics = await getHealthMetrics(DB, platform, 24);
625
+
626
+ try {
627
+ await DB.prepare(`
628
+ INSERT INTO health_reports (report_date, platform, events_sent, events_failed, success_rate, errors_detected, issues_detected)
629
+ VALUES (?, ?, ?, ?, ?, ?, ?)
630
+ `).bind(
631
+ today, platform,
632
+ metrics.events_sent, metrics.events_failed, metrics.success_rate,
633
+ JSON.stringify(metrics.errors_detected), JSON.stringify(metrics.issues)
634
+ ).run();
635
+ reports.push({ platform, status: 'ok' });
636
+ } catch (err: any) {
637
+ console.error(`Failed to generate report for ${platform}:`, err?.message || String(err));
638
+ reports.push({ platform, status: 'failed' });
639
+ }
640
+ }
641
+
642
+ return reports;
643
+ }
644
+
645
+ 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> {
646
+ if (!DB) return;
647
+ try {
648
+ await DB.prepare(`
649
+ INSERT INTO intelligence_logs (run_type, platform, check_type, status, current_value, expected_value, message, alert_sent)
650
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
651
+ `).bind(runType, platform, checkType, status, String(currentValue ?? ''), String(expectedValue ?? ''), message, alertSent ? 1 : 0).run();
652
+ } catch (err: any) {
653
+ console.error('logIntelligence error:', err?.message || String(err));
654
+ }
655
+ }
656
+
657
+ // ── Webhook Processing Helper ─────────────────────────────────────────────────
658
+ /**
659
+ * Processa webhook de e-commerce (Hotmart, Kiwify, Ticto, etc.)
660
+ * - Verifica duplicatas
661
+ * - Registra evento em webhook_events
662
+ * - Retorna true se já foi processado, false se deve continuar
663
+ */
664
+ export async function processWebhookDuplicateCheck(
665
+ env: Env,
666
+ platform: string,
667
+ transactionId: string,
668
+ rawPayload: string,
669
+ context?: { email?: string; orderId?: string }
670
+ ): Promise<{ duplicate: boolean; error?: string }> {
671
+ if (!env.DB) {
672
+ return { duplicate: false };
673
+ }
674
+
675
+ try {
676
+ // 1. Verifica duplicatas
677
+ const dup = await env.DB.prepare(
678
+ 'SELECT id FROM webhook_events WHERE transaction_id = ? AND platform = ? AND status = ?'
679
+ ).bind(transactionId, platform, 'processed').first();
680
+
681
+ if (dup) {
682
+ console.log(`[Webhook] Duplicate event skipped: ${platform}/${transactionId}`);
683
+ return { duplicate: true };
684
+ }
685
+
686
+ // 2. Registra o evento
687
+ await env.DB.prepare(
688
+ 'INSERT OR IGNORE INTO webhook_events (platform, transaction_id, email, status, raw_payload) VALUES (?,?,?,?,?)'
689
+ ).bind(platform, transactionId, context?.email || null, 'processed', rawPayload).run();
690
+
691
+ return { duplicate: false };
692
+ } catch (err: any) {
693
+ console.error(`[Webhook] Error processing ${platform}/${transactionId}:`, {
694
+ platform,
695
+ transactionId,
696
+ email: context?.email,
697
+ error: err?.message || String(err),
698
+ stack: err?.stack,
699
+ });
700
+ return { duplicate: false, error: err?.message || String(err) };
701
+ }
702
+ }