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,72 @@
1
+ /**
2
+ * CDP Edge — GA4 Measurement Protocol
3
+ * Envia eventos server-side para o GA4 via Measurement Protocol.
4
+ */
5
+
6
+ import { normalizePhone } from '../utils.js';
7
+ import { logApiFailure } from '../db.js';
8
+ import { Env, TrackPayload } from '../../types.js';
9
+ import { ExecutionContext } from '@cloudflare/workers-types';
10
+
11
+ export async function sendGA4Mp(env: Env, ga4EventName: string, payload: TrackPayload, ctx: ExecutionContext | null): Promise<{ ok?: boolean; status?: number; skipped?: string; error?: string }> {
12
+ if (!env.GA4_API_SECRET) return { skipped: 'GA4_API_SECRET not set' };
13
+
14
+ const {
15
+ gaClientId: clientId, sessionId,
16
+ value, currency, contentName,
17
+ email, phone, firstName,
18
+ orderId,
19
+ } = payload;
20
+
21
+ if (!clientId) return { skipped: 'no clientId' };
22
+
23
+ const eventParams: Record<string, string | number> = {
24
+ ...(value !== undefined && { value: parseFloat(String(value)) }),
25
+ ...(currency && { currency: String(currency).toUpperCase() }),
26
+ ...(contentName && { content_name: contentName }),
27
+ ...(orderId && { transaction_id: orderId }),
28
+ ...(email && { user_data_email_address: email.toLowerCase().trim() }),
29
+ ...(phone && { user_data_phone_number: normalizePhone(phone) || '' }),
30
+ ...(firstName && { user_data_first_name: firstName.toLowerCase().trim() }),
31
+ ...(sessionId && { session_id: sessionId }),
32
+ engagement_time_msec: 100,
33
+ };
34
+
35
+ const body = {
36
+ client_id: clientId,
37
+ events: [{ name: ga4EventName, params: eventParams }],
38
+ };
39
+
40
+ const url = `https://www.google-analytics.com/mp/collect`
41
+ + `?measurement_id=${env.GA4_MEASUREMENT_ID}`
42
+ + `&api_secret=${env.GA4_API_SECRET}`;
43
+
44
+ try {
45
+ const res = await fetch(url, {
46
+ method: 'POST',
47
+ headers: { 'Content-Type': 'application/json' },
48
+ body: JSON.stringify(body),
49
+ });
50
+
51
+ if (res.status !== 204) {
52
+ if (env.DB && ctx) {
53
+ ctx.waitUntil(logApiFailure(env.DB, 'ga4', ga4EventName, String(res.status), 'GA4 returned non-204 status', '', JSON.stringify(body)));
54
+ }
55
+ }
56
+
57
+ return res.status === 204 ? { ok: true } : { status: res.status };
58
+ } catch (err: any) {
59
+ console.error('GA4 MP fetch failed:', err?.message || String(err));
60
+
61
+ if (env.DB && ctx) {
62
+ ctx.waitUntil(logApiFailure(env.DB, 'ga4', ga4EventName, 'FETCH_ERROR', err?.message || String(err), '', JSON.stringify(body)));
63
+ }
64
+
65
+ if (env.RETRY_QUEUE) {
66
+ const send = env.RETRY_QUEUE.send({ eventType: ga4EventName, payload, platform: 'ga4' });
67
+ if (ctx) ctx.waitUntil(send); else send.catch(() => {});
68
+ }
69
+
70
+ return { error: err?.message || String(err) };
71
+ }
72
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * CDP Edge — Meta Conversions API v22.0
3
+ * Envia eventos server-side para a Meta CAPI.
4
+ */
5
+
6
+ import { sha256, normalizePhone, normalizeCity } from '../utils.js';
7
+ import { logApiFailure } from '../db.js';
8
+ import { logMatchQuality, autoEnrichPayload } from '../ml/matchquality.js';
9
+ import { Env, TrackPayload } from '../../types.js';
10
+ import { ExecutionContext } from '@cloudflare/workers-types';
11
+
12
+ interface EnrichedPayload {
13
+ payload: TrackPayload;
14
+ recovered: { email: boolean; utm: boolean };
15
+ }
16
+
17
+ export async function sendMetaCapi(env: Env, eventName: string, payload: TrackPayload, request: Request | null, ctx: ExecutionContext | null): Promise<any> {
18
+ // Auto-enriquecer payload com dados do Identity Graph antes do envio
19
+ let recovered = { email: false, utm: false };
20
+ if (env.DB && payload) {
21
+ const enriched = await autoEnrichPayload(env, payload) as EnrichedPayload;
22
+ payload = enriched.payload;
23
+ recovered = enriched.recovered;
24
+ }
25
+
26
+ const {
27
+ email, phone, firstName, lastName,
28
+ city, state, country,
29
+ zip, dob,
30
+ fbp, fbc, userId,
31
+ eventId, pageUrl,
32
+ value, currency,
33
+ contentIds, contentName, contentType, numItems,
34
+ // Dual-layer context — funil avançado + imóveis
35
+ funnel_stage, distanceBucket: distance_bucket, intentScoreNum: intent_score, intent_bucket,
36
+ ltvScore, ltvClass, metaSignal, metaSignalBucket: metaSignalBucketVal,
37
+ } = payload;
38
+
39
+ const phoneNorm = normalizePhone(phone);
40
+ const countryCode = (country || (request as any)?.cf?.country || 'br').toLowerCase();
41
+ const stateCode = state ? String(state).toLowerCase() : undefined;
42
+ const cityNorm = normalizeCity(city);
43
+
44
+ const userData: Record<string, string> = {
45
+ ...(email && { em: await sha256(email) || '' }),
46
+ ...(phoneNorm && { ph: await sha256(phoneNorm) || '' }),
47
+ ...(firstName && { fn: await sha256(firstName) || '' }),
48
+ ...(lastName && { ln: await sha256(lastName) || '' }),
49
+ ...(cityNorm && { ct: await sha256(cityNorm) || '' }),
50
+ ...(stateCode && { st: await sha256(stateCode) || '' }),
51
+ ...(countryCode && { country: await sha256(countryCode) || '' }),
52
+ ...(userId && { external_id: await sha256(String(userId)) || '' }),
53
+ ...(zip && { zp: await sha256(zip) || '' }),
54
+ ...(dob && { db: await sha256(dob) || '' }),
55
+ ...(fbp && { fbp }),
56
+ ...(fbc && { fbc }),
57
+ client_ip_address: request?.headers.get('CF-Connecting-IP')
58
+ || request?.headers.get('X-Forwarded-For')
59
+ || '',
60
+ client_user_agent: request?.headers.get('User-Agent') || '',
61
+ };
62
+
63
+ const customData: Record<string, string | number | string[]> = {
64
+ ...(value !== undefined && { value: parseFloat(String(value)) }),
65
+ ...(currency && { currency: String(currency).toUpperCase() }),
66
+ ...(contentIds && contentIds.length > 0 && { content_ids: contentIds }),
67
+ ...(contentName && { content_name: contentName }),
68
+ ...(contentType && { content_type: contentType }),
69
+ ...(numItems && { num_items: parseInt(String(numItems)) }),
70
+ // Contexto de funil e proximidade — enriquece matching e otimização Meta
71
+ ...(funnel_stage && { funnel_stage }),
72
+ ...(distance_bucket && { distance_bucket }),
73
+ ...(intent_score && { intent_score }),
74
+ ...(ltvScore !== undefined && ltvScore !== null && { ltv_score: ltvScore }),
75
+ ...(ltvClass && { ltv_class: ltvClass }),
76
+ ...(metaSignal !== undefined && metaSignal !== null && { meta_signal: metaSignal }),
77
+ ...(metaSignalBucketVal && { meta_signal_bucket: metaSignalBucketVal }),
78
+ ...(intent_bucket && { intent_bucket }),
79
+ };
80
+
81
+ const eventPayload = {
82
+ event_name: eventName,
83
+ event_time: Math.floor(Date.now() / 1000),
84
+ event_id: eventId || `cdp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
85
+ event_source_url: pageUrl || `https://${env.SITE_DOMAIN}`,
86
+ action_source: 'website',
87
+ user_data: userData,
88
+ ...(Object.keys(customData).length > 0 && { custom_data: customData }),
89
+ };
90
+
91
+ const requestBody: Record<string, any> = {
92
+ data: [eventPayload],
93
+ access_token: env.META_ACCESS_TOKEN,
94
+ };
95
+
96
+ if (env.META_TEST_CODE) {
97
+ requestBody.test_event_code = env.META_TEST_CODE;
98
+ }
99
+
100
+ // Logar match quality em background (não bloqueia dispatch)
101
+ if (env.DB && ctx) {
102
+ ctx.waitUntil(logMatchQuality(env.DB, eventName, payload, recovered));
103
+ } else if (env.DB) {
104
+ logMatchQuality(env.DB, eventName, payload, recovered).catch(() => {});
105
+ }
106
+
107
+ const endpoint = `https://graph.facebook.com/v22.0/${env.META_PIXEL_ID}/events`;
108
+
109
+ try {
110
+ const res = await fetch(endpoint, {
111
+ method: 'POST',
112
+ headers: { 'Content-Type': 'application/json' },
113
+ body: JSON.stringify(requestBody),
114
+ });
115
+
116
+ const data = await res.json();
117
+
118
+ if (!res.ok) {
119
+ const errorCode = (data as any).error?.code || String(res.status);
120
+ const errorMessage = (data as any).error?.message || (data as any).error?.error_user_msg || 'Unknown error';
121
+ console.error('Meta CAPI error:', res.status, errorMessage);
122
+
123
+ if (env.DB && ctx) {
124
+ ctx.waitUntil(logApiFailure(env.DB, 'meta', eventName, errorCode, errorMessage, eventPayload.event_id, JSON.stringify(requestBody)));
125
+ }
126
+ }
127
+
128
+ return data;
129
+ } catch (err: any) {
130
+ console.error('Meta CAPI fetch failed:', err?.message || String(err));
131
+
132
+ if (env.DB && ctx) {
133
+ ctx.waitUntil(logApiFailure(env.DB, 'meta', eventName, 'FETCH_ERROR', err?.message || String(err), eventPayload.event_id, JSON.stringify(requestBody)));
134
+ }
135
+
136
+ if (env.RETRY_QUEUE) {
137
+ const send = env.RETRY_QUEUE.send({ eventType: eventName, payload, platform: 'meta' });
138
+ if (ctx) ctx.waitUntil(send); else send.catch(() => {});
139
+ }
140
+
141
+ return { error: err?.message || String(err) };
142
+ }
143
+ }
@@ -0,0 +1,255 @@
1
+ /**
2
+ * CDP Edge — Plataformas Adicionais
3
+ * Pinterest CAPI v5, Reddit CAPI v2.0, LinkedIn CAPI 202401, Spotify CAPI v1
4
+ */
5
+
6
+ import { sha256, normalizePhone } from '../utils.js';
7
+ import { logApiFailure } from '../db.js';
8
+ import { Env, TrackPayload } from '../../types.js';
9
+ import { ExecutionContext } from '@cloudflare/workers-types';
10
+
11
+ // ── Pinterest Conversions API v5 ──────────────────────────────────────────────
12
+ export async function sendPinterestCapi(env: Env, eventName: string, payload: TrackPayload, request: Request | null, ctx: ExecutionContext | null): Promise<any> {
13
+ if (!env.PINTEREST_ACCESS_TOKEN || !env.PINTEREST_AD_ACCOUNT_ID) {
14
+ return { skipped: 'Pinterest credentials not set' };
15
+ }
16
+
17
+ const { email, phone, userId, eventId, pageUrl, value, currency, contentIds, contentName } = payload;
18
+ const phoneNorm = normalizePhone(phone);
19
+
20
+ const pinterestEventMap: Record<string, string> = {
21
+ PageView: 'pagevisit', ViewContent: 'pagevisit', Lead: 'lead', Purchase: 'checkout',
22
+ AddToCart: 'addtocart', InitiateCheckout: 'checkout', CompleteRegistration: 'signup',
23
+ Search: 'search', Contact: 'lead',
24
+ };
25
+ const pEvent = pinterestEventMap[eventName] || 'custom';
26
+
27
+ const userData: Record<string, string | string[]> = {
28
+ ...(email && { em: [await sha256(email) || ''] }),
29
+ ...(phoneNorm && { ph: [await sha256(phoneNorm) || ''] }),
30
+ ...(userId && { external_id: [await sha256(String(userId)) || ''] }),
31
+ client_ip_address: request?.headers.get('CF-Connecting-IP') || '',
32
+ client_user_agent: request?.headers.get('User-Agent') || '',
33
+ };
34
+
35
+ const body = {
36
+ data: [{
37
+ event_name: pEvent,
38
+ action_source: 'web',
39
+ event_time: Math.floor(Date.now() / 1000),
40
+ event_id: eventId || `cdp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
41
+ event_source_url: pageUrl || '',
42
+ user_data: userData,
43
+ custom_data: {
44
+ currency: (currency || 'BRL').toUpperCase(),
45
+ value: value ? String(parseFloat(String(value))) : '0',
46
+ ...(contentIds && contentIds.length > 0 && { content_ids: contentIds.map(String) }),
47
+ ...(contentName && { content_name: contentName }),
48
+ content_type: 'product',
49
+ },
50
+ }],
51
+ };
52
+
53
+ try {
54
+ const res = await fetch(
55
+ `https://api.pinterest.com/v5/ad_accounts/${env.PINTEREST_AD_ACCOUNT_ID}/events`,
56
+ { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${env.PINTEREST_ACCESS_TOKEN}` }, body: JSON.stringify(body) }
57
+ );
58
+ const data = await res.json();
59
+ if (!res.ok) {
60
+ const msg = (data as any).message || (data as any).code || String(res.status);
61
+ console.error('Pinterest CAPI error:', res.status, msg);
62
+ if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'pinterest', eventName, String(res.status), msg, (body.data as any)[0].event_id, JSON.stringify(body)));
63
+ }
64
+ return data;
65
+ } catch (err: any) {
66
+ console.error('Pinterest CAPI fetch failed:', err?.message || String(err));
67
+ if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'pinterest', eventName, 'FETCH_ERROR', err?.message || String(err), '', JSON.stringify(body)));
68
+ if (env.RETRY_QUEUE) {
69
+ const send = env.RETRY_QUEUE.send({ eventType: eventName, payload, platform: 'pinterest' });
70
+ if (ctx) ctx.waitUntil(send); else send.catch(() => {});
71
+ }
72
+ return { error: err?.message || String(err) };
73
+ }
74
+ }
75
+
76
+ // ── Reddit Conversions API v2.0 ───────────────────────────────────────────────
77
+ export async function sendRedditCapi(env: Env, eventName: string, payload: TrackPayload, request: Request | null, ctx: ExecutionContext | null): Promise<any> {
78
+ if (!env.REDDIT_ACCESS_TOKEN || !env.REDDIT_AD_ACCOUNT_ID) {
79
+ return { skipped: 'Reddit credentials not set' };
80
+ }
81
+
82
+ const { email, phone, userId, eventId, pageUrl, value, currency } = payload;
83
+ const phoneNorm = normalizePhone(phone);
84
+
85
+ const redditEventMap: Record<string, string> = {
86
+ PageView: 'PageVisit', ViewContent: 'ViewContent', Lead: 'Lead', Purchase: 'Purchase',
87
+ AddToCart: 'AddToCart', InitiateCheckout: 'Purchase', CompleteRegistration: 'SignUp',
88
+ Search: 'Search', Contact: 'Lead',
89
+ };
90
+ const rEvent = redditEventMap[eventName] || 'Custom';
91
+
92
+ const user: Record<string, { value: string }> = {
93
+ ...(email && { email: { value: await sha256(email) || '' } }),
94
+ ...(phoneNorm && { phoneNumber: { value: await sha256(phoneNorm) || '' } }),
95
+ ...(userId && { externalId: { value: await sha256(String(userId)) || '' } }),
96
+ ipAddress: { value: request?.headers.get('CF-Connecting-IP') || '' },
97
+ userAgent: { value: request?.headers.get('User-Agent') || '' },
98
+ };
99
+
100
+ const event: Record<string, any> = {
101
+ event_at: new Date().toISOString(),
102
+ event_type: { tracking_type: rEvent, ...(eventName === 'InitiateCheckout' && { conversion_type: 'BEGIN_CHECKOUT' }) },
103
+ click_id: (payload as any).rdtClid || '',
104
+ event_metadata: {
105
+ currency: (currency || 'BRL').toUpperCase(),
106
+ value_decimal: String(value || 0),
107
+ item_count: '1',
108
+ conversion_id: eventId || `cdp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
109
+ },
110
+ user,
111
+ };
112
+
113
+ const body = { events: [event] };
114
+
115
+ try {
116
+ const res = await fetch(
117
+ `https://ads-api.reddit.com/api/v2.0/conversions/events/${env.REDDIT_AD_ACCOUNT_ID}`,
118
+ { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${env.REDDIT_ACCESS_TOKEN}` }, body: JSON.stringify(body) }
119
+ );
120
+ if (!res.ok) {
121
+ const txt = await res.text();
122
+ console.error('Reddit CAPI error:', txt);
123
+ if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'reddit', eventName, String(res.status), txt, event.event_metadata.conversion_id, JSON.stringify(body)));
124
+ return { error: `HTTP ${res.status}` };
125
+ }
126
+ return await res.json();
127
+ } catch (err: any) {
128
+ console.error('Reddit CAPI fetch failed:', err?.message || String(err));
129
+ if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'reddit', eventName, 'FETCH_ERROR', err?.message || String(err), '', JSON.stringify(body)));
130
+ if (env.RETRY_QUEUE) {
131
+ const send = env.RETRY_QUEUE.send({ eventType: eventName, payload, platform: 'reddit' });
132
+ if (ctx) ctx.waitUntil(send); else send.catch(() => {});
133
+ }
134
+ return { error: err?.message || String(err) };
135
+ }
136
+ }
137
+
138
+ // ── LinkedIn Conversions API (LinkedIn-Version: 202401) ───────────────────────
139
+ export async function sendLinkedInCapi(env: Env, eventName: string, payload: TrackPayload, request: Request | null, ctx: ExecutionContext | null): Promise<any> {
140
+ if (!env.LINKEDIN_ACCESS_TOKEN || !env.LINKEDIN_CONVERSION_ID) {
141
+ return { skipped: 'LinkedIn credentials not set' };
142
+ }
143
+
144
+ const { email, phone, firstName, lastName, userId, eventId, pageUrl, value, currency } = payload;
145
+ const phoneNorm = normalizePhone(phone);
146
+
147
+ const linkedInEventMap: Record<string, string> = {
148
+ Lead: 'LEAD', Purchase: 'PURCHASE', CompleteRegistration: 'REGISTRATION',
149
+ AddToCart: 'ADD_TO_CART', InitiateCheckout: 'OTHER', ViewContent: 'OTHER',
150
+ PageView: 'OTHER', Contact: 'LEAD',
151
+ };
152
+
153
+ const userInfo: Record<string, string> = {
154
+ ...(email && { 'SHA256_EMAIL': await sha256(email) || '' }),
155
+ ...(phoneNorm && { 'SHA256_PHONE': await sha256(phoneNorm) || '' }),
156
+ ...(firstName && { 'SHA256_FIRST_NAME': await sha256(firstName.toLowerCase().trim()) || '' }),
157
+ ...(lastName && { 'SHA256_LAST_NAME': await sha256(lastName.toLowerCase().trim()) || '' }),
158
+ };
159
+
160
+ const body: Record<string, any> = {
161
+ conversion: `urn:li:conversion:${env.LINKEDIN_CONVERSION_ID}`,
162
+ conversionHappenedAt: Date.now(),
163
+ conversionValue: value ? { currencyCode: (currency || 'BRL').toUpperCase(), amount: String(parseFloat(String(value))) } : undefined,
164
+ eventId: eventId || `cdp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
165
+ ...(Object.keys(userInfo).length > 0 && { user: { userIds: Object.entries(userInfo).map(([idType, idValue]) => ({ idType, idValue })) } }),
166
+ };
167
+
168
+ try {
169
+ const res = await fetch('https://api.linkedin.com/rest/conversionEvents', {
170
+ method: 'POST',
171
+ headers: {
172
+ 'Content-Type': 'application/json',
173
+ Authorization: `Bearer ${env.LINKEDIN_ACCESS_TOKEN}`,
174
+ 'LinkedIn-Version': '202401',
175
+ 'X-Restli-Protocol-Version': '2.0.0',
176
+ },
177
+ body: JSON.stringify(body),
178
+ });
179
+ if (!res.ok) {
180
+ const txt = await res.text();
181
+ console.error('LinkedIn CAPI error:', txt);
182
+ if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'linkedin', eventName, String(res.status), txt, body.eventId, JSON.stringify(body)));
183
+ return { error: `HTTP ${res.status}` };
184
+ }
185
+ return { ok: true };
186
+ } catch (err: any) {
187
+ console.error('LinkedIn CAPI fetch failed:', err?.message || String(err));
188
+ if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'linkedin', eventName, 'FETCH_ERROR', err?.message || String(err), '', JSON.stringify(body)));
189
+ if (env.RETRY_QUEUE) {
190
+ const send = env.RETRY_QUEUE.send({ eventType: eventName, payload, platform: 'linkedin' });
191
+ if (ctx) ctx.waitUntil(send); else send.catch(() => {});
192
+ }
193
+ return { error: err?.message || String(err) };
194
+ }
195
+ }
196
+
197
+ // ── Spotify Conversions API v1 ────────────────────────────────────────────────
198
+ export async function sendSpotifyCapi(env: Env, eventName: string, payload: TrackPayload, request: Request | null, ctx: ExecutionContext | null): Promise<any> {
199
+ if (!env.SPOTIFY_ACCESS_TOKEN || !env.SPOTIFY_AD_ACCOUNT_ID) {
200
+ return { skipped: 'Spotify credentials not set' };
201
+ }
202
+
203
+ const { email, phone, userId, eventId, pageUrl, value, currency } = payload;
204
+ const phoneNorm = normalizePhone(phone);
205
+
206
+ const spotifyEventMap: Record<string, string> = {
207
+ Purchase: 'PURCHASE', Lead: 'LEAD', CompleteRegistration: 'SIGN_UP',
208
+ AddToCart: 'ADD_TO_CART', InitiateCheckout: 'INITIATE_CHECKOUT',
209
+ ViewContent: 'VIEW_CONTENT', PageView: 'PAGE_VIEW', Contact: 'LEAD',
210
+ };
211
+ const spEvent = spotifyEventMap[eventName] || 'CUSTOM';
212
+
213
+ const user: Record<string, string> = {
214
+ ...(email && { hashed_email: await sha256(email) || '' }),
215
+ ...(phoneNorm && { hashed_phone: await sha256(phoneNorm) || '' }),
216
+ ...(userId && { user_id: userId }),
217
+ ip_address: request?.headers.get('CF-Connecting-IP') || '',
218
+ user_agent: request?.headers.get('User-Agent') || '',
219
+ };
220
+
221
+ const body = {
222
+ data: [{
223
+ event_id: eventId || `cdp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
224
+ event_type: spEvent,
225
+ event_time: Math.floor(Date.now() / 1000),
226
+ url: pageUrl || '',
227
+ user,
228
+ ...(value !== undefined && {
229
+ value: { currency: (currency || 'BRL').toUpperCase(), amount: parseFloat(String(value)) },
230
+ }),
231
+ }],
232
+ };
233
+
234
+ try {
235
+ const res = await fetch(
236
+ `https://advertising-api.spotify.com/conversion/v1/accounts/${env.SPOTIFY_AD_ACCOUNT_ID}/events`,
237
+ { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${env.SPOTIFY_ACCESS_TOKEN}` }, body: JSON.stringify(body) }
238
+ );
239
+ if (!res.ok) {
240
+ const txt = await res.text();
241
+ console.error('Spotify CAPI error:', txt);
242
+ if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'spotify', eventName, String(res.status), txt, (body.data as any)[0].event_id, JSON.stringify(body)));
243
+ return { error: `HTTP ${res.status}` };
244
+ }
245
+ return await res.json();
246
+ } catch (err: any) {
247
+ console.error('Spotify CAPI fetch failed:', err?.message || String(err));
248
+ if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'spotify', eventName, 'FETCH_ERROR', err?.message || String(err), '', JSON.stringify(body)));
249
+ if (env.RETRY_QUEUE) {
250
+ const send = env.RETRY_QUEUE.send({ eventType: eventName, payload, platform: 'spotify' });
251
+ if (ctx) ctx.waitUntil(send); else send.catch(() => {});
252
+ }
253
+ return { error: err?.message || String(err) };
254
+ }
255
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * CDP Edge — TikTok Events API v1.3
3
+ * Envia eventos server-side para a TikTok Events API.
4
+ */
5
+
6
+ import { sha256, normalizePhone } from '../utils.js';
7
+ import { logApiFailure } from '../db.js';
8
+ import { Env, TrackPayload } from '../../types.js';
9
+ import { ExecutionContext } from '@cloudflare/workers-types';
10
+
11
+ export async function sendTikTokApi(env: Env, eventName: string, payload: TrackPayload, request: Request | null, ctx: ExecutionContext | null): Promise<any> {
12
+ if (!env.TIKTOK_ACCESS_TOKEN) return { skipped: 'TIKTOK_ACCESS_TOKEN not set' };
13
+
14
+ const pixelId = env.TIKTOK_PIXEL_ID;
15
+ if (!pixelId || pixelId === 'SEU_TIKTOK_PIXEL_ID') return { skipped: 'TIKTOK_PIXEL_ID not configured' };
16
+
17
+ const {
18
+ email, phone, firstName, lastName,
19
+ fbp, fbc, ttp, ttclid, userId,
20
+ eventId, pageUrl,
21
+ value, currency,
22
+ contentIds, contentName, contentType,
23
+ } = payload;
24
+
25
+ const phoneNorm = normalizePhone(phone);
26
+
27
+ const user: Record<string, string> = {
28
+ ...(email && { email: await sha256(email) || '' }),
29
+ ...(phoneNorm && { phone_number: await sha256(phoneNorm) || '' }),
30
+ ...(userId && { external_id: await sha256(String(userId)) || '' }),
31
+ ...(ttp && { ttp }),
32
+ ...(ttclid && { ttclid }),
33
+ };
34
+
35
+ const properties: Record<string, any> = {
36
+ ...(value !== undefined && { value: parseFloat(String(value)) }),
37
+ ...(currency && { currency: String(currency).toUpperCase() }),
38
+ ...(contentIds && contentIds && contentIds.length > 0 && {
39
+ contents: contentIds.map(id => ({
40
+ content_id: String(id),
41
+ content_name: contentName || '',
42
+ content_type: contentType || 'product',
43
+ quantity: 1,
44
+ price: value ? parseFloat(String(value)) : 0,
45
+ })),
46
+ }),
47
+ };
48
+
49
+ const event: Record<string, any> = {
50
+ event: eventName,
51
+ event_time: Math.floor(Date.now() / 1000),
52
+ event_id: eventId || `cdp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
53
+ user,
54
+ page: {
55
+ url: pageUrl || `https://${env.SITE_DOMAIN}`,
56
+ referrer: request?.headers.get('Referer') || '',
57
+ },
58
+ ...(Object.keys(properties).length > 0 && { properties }),
59
+ context: {
60
+ ip: request?.headers.get('CF-Connecting-IP') || '',
61
+ user_agent: request?.headers.get('User-Agent') || '',
62
+ },
63
+ };
64
+
65
+ const body = {
66
+ event_source: 'web',
67
+ event_source_id: pixelId,
68
+ data: [event],
69
+ };
70
+
71
+ // Endpoint canônico: sempre /v1.3/event/track/
72
+ const endpoint = 'https://business-api.tiktok.com/open_api/v1.3/event/track/';
73
+
74
+ try {
75
+ const res = await fetch(endpoint, {
76
+ method: 'POST',
77
+ headers: {
78
+ 'Content-Type': 'application/json',
79
+ 'Access-Token': env.TIKTOK_ACCESS_TOKEN,
80
+ },
81
+ body: JSON.stringify(body),
82
+ });
83
+
84
+ const data = await res.json();
85
+ if (!res.ok || (data as any).code !== 0) {
86
+ console.error('TikTok Events API error:', res.status, (data as any).message || (data as any).code || 'unknown');
87
+
88
+ if (env.DB && ctx) {
89
+ ctx.waitUntil(logApiFailure(env.DB, 'tiktok', eventName, String((data as any).code || res.status), (data as any).message || 'TikTok API error', event.event_id, JSON.stringify(body)));
90
+ }
91
+ }
92
+ return data;
93
+ } catch (err: any) {
94
+ console.error('TikTok Events API fetch failed:', err?.message || String(err));
95
+
96
+ if (env.DB && ctx) {
97
+ ctx.waitUntil(logApiFailure(env.DB, 'tiktok', eventName, 'FETCH_ERROR', err?.message || String(err), '', JSON.stringify(body)));
98
+ }
99
+
100
+ if (env.RETRY_QUEUE) {
101
+ const send = env.RETRY_QUEUE.send({ eventType: eventName, payload, platform: 'tiktok' });
102
+ if (ctx) ctx.waitUntil(send); else send.catch(() => {});
103
+ }
104
+
105
+ return { error: err?.message || String(err) };
106
+ }
107
+ }