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.
- package/README.md +324 -0
- package/bin/cdp-edge.js +71 -0
- package/contracts/agent-versions.json +679 -0
- package/contracts/api-versions.json +372 -0
- package/contracts/types.ts +81 -0
- package/dist/commands/analyze.js +52 -0
- package/dist/commands/infra.js +54 -0
- package/dist/commands/install.js +191 -0
- package/dist/commands/server.js +174 -0
- package/dist/commands/setup.js +355 -0
- package/dist/commands/validate.js +248 -0
- package/dist/index.js +12 -0
- package/dist/sdk/cdpTrack.js +2095 -0
- package/dist/sdk/cdpTrack.min.js +64 -0
- package/dist/sdk/install-snippet.html +10 -0
- package/docs/CI-CD-SETUP.md +217 -0
- package/docs/events-reference.md +359 -0
- package/docs/installation.md +155 -0
- package/docs/quick-start.md +185 -0
- package/docs/sdk-reference.md +371 -0
- package/docs/whatsapp-ctwa.md +210 -0
- package/extracted-skill/tracking-events-generator/INDEX.md +94 -0
- package/extracted-skill/tracking-events-generator/INSTALACAO-CDPEDGE.md +58 -0
- package/extracted-skill/tracking-events-generator/INTEGRACAO-COMPLETA.md +683 -0
- package/extracted-skill/tracking-events-generator/MELHORIAS-IMPLEMENTADAS.md +513 -0
- package/extracted-skill/tracking-events-generator/Premium-Tracking-Intelligence-Resumo.md +333 -0
- package/extracted-skill/tracking-events-generator/SKILL.md +257 -0
- package/extracted-skill/tracking-events-generator/advanced-matching.js +364 -0
- package/extracted-skill/tracking-events-generator/agents/ab-ltv-agent.md +196 -0
- package/extracted-skill/tracking-events-generator/agents/ab-testing-agent.md +54 -0
- package/extracted-skill/tracking-events-generator/agents/attribution-agent.md +1304 -0
- package/extracted-skill/tracking-events-generator/agents/bidding-agent.md +347 -0
- package/extracted-skill/tracking-events-generator/agents/bing-agent.md +66 -0
- package/extracted-skill/tracking-events-generator/agents/browser-tracking.md +364 -0
- package/extracted-skill/tracking-events-generator/agents/code-guardian-agent.md +149 -0
- package/extracted-skill/tracking-events-generator/agents/compliance-agent.md +2097 -0
- package/extracted-skill/tracking-events-generator/agents/crm-integration-agent.md +1459 -0
- package/extracted-skill/tracking-events-generator/agents/dashboard-agent.md +456 -0
- package/extracted-skill/tracking-events-generator/agents/database-agent.md +668 -0
- package/extracted-skill/tracking-events-generator/agents/debug-agent.md +1455 -0
- package/extracted-skill/tracking-events-generator/agents/devops-agent.md +232 -0
- package/extracted-skill/tracking-events-generator/agents/domain-setup-agent.md +238 -0
- package/extracted-skill/tracking-events-generator/agents/email-agent.md +88 -0
- package/extracted-skill/tracking-events-generator/agents/fingerprint-agent.md +257 -0
- package/extracted-skill/tracking-events-generator/agents/fraud-detection-agent.md +143 -0
- package/extracted-skill/tracking-events-generator/agents/google-agent.md +235 -0
- package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +525 -0
- package/extracted-skill/tracking-events-generator/agents/lead-scoring-agent.md +282 -0
- package/extracted-skill/tracking-events-generator/agents/linkedin-agent.md +173 -0
- package/extracted-skill/tracking-events-generator/agents/localization-agent.md +55 -0
- package/extracted-skill/tracking-events-generator/agents/ltv-predictor-agent.md +59 -0
- package/extracted-skill/tracking-events-generator/agents/master-feedback-loop.md +960 -0
- package/extracted-skill/tracking-events-generator/agents/master-orchestrator.md +2154 -0
- package/extracted-skill/tracking-events-generator/agents/match-quality-agent.md +304 -0
- package/extracted-skill/tracking-events-generator/agents/memory-agent.json +25 -0
- package/extracted-skill/tracking-events-generator/agents/memory-agent.md +878 -0
- package/extracted-skill/tracking-events-generator/agents/meta-agent.md +118 -0
- package/extracted-skill/tracking-events-generator/agents/ml-clustering-agent.md +749 -0
- package/extracted-skill/tracking-events-generator/agents/page-analyzer.md +272 -0
- package/extracted-skill/tracking-events-generator/agents/performance-agent.md +1167 -0
- package/extracted-skill/tracking-events-generator/agents/performance-optimization-agent.md +1442 -0
- package/extracted-skill/tracking-events-generator/agents/pinterest-agent.md +318 -0
- package/extracted-skill/tracking-events-generator/agents/premium-tracking-intelligence-agent.md +849 -0
- package/extracted-skill/tracking-events-generator/agents/r2-setup-agent.md +258 -0
- package/extracted-skill/tracking-events-generator/agents/reddit-agent.md +321 -0
- package/extracted-skill/tracking-events-generator/agents/security-enterprise-agent.md +1861 -0
- package/extracted-skill/tracking-events-generator/agents/server-tracking.md +1188 -0
- package/extracted-skill/tracking-events-generator/agents/spotify-agent.md +391 -0
- package/extracted-skill/tracking-events-generator/agents/tiktok-agent.md +182 -0
- package/extracted-skill/tracking-events-generator/agents/tracking-plan-agent.md +459 -0
- package/extracted-skill/tracking-events-generator/agents/utm-agent.md +322 -0
- package/extracted-skill/tracking-events-generator/agents/validator-agent.md +271 -0
- package/extracted-skill/tracking-events-generator/agents/webhook-agent.md +177 -0
- package/extracted-skill/tracking-events-generator/agents/whatsapp-agent.md +129 -0
- package/extracted-skill/tracking-events-generator/agents/whatsapp-ctwa-setup-agent.md +707 -0
- package/extracted-skill/tracking-events-generator/agents/youtube-agent.md +537 -0
- package/extracted-skill/tracking-events-generator/anti-blocking.js +285 -0
- package/extracted-skill/tracking-events-generator/cdpTrack.js +640 -0
- package/extracted-skill/tracking-events-generator/contracts/api-versions.json +372 -0
- package/extracted-skill/tracking-events-generator/docs/guia-cloudflare-iniciante.md +107 -0
- package/extracted-skill/tracking-events-generator/engagement-scoring.js +226 -0
- package/extracted-skill/tracking-events-generator/evals/evals.json +235 -0
- package/extracted-skill/tracking-events-generator/integration-test.js +497 -0
- package/extracted-skill/tracking-events-generator/knowledge-base.md +3066 -0
- package/extracted-skill/tracking-events-generator/micro-events.js +992 -0
- package/extracted-skill/tracking-events-generator/models/captura-de-lead.md +78 -0
- package/extracted-skill/tracking-events-generator/models/captura-lead-evento-externo.md +99 -0
- package/extracted-skill/tracking-events-generator/models/checkout-proprio.md +111 -0
- package/extracted-skill/tracking-events-generator/models/lancamento-imobiliario.md +344 -0
- package/extracted-skill/tracking-events-generator/models/multi-step-checkout.md +672 -0
- package/extracted-skill/tracking-events-generator/models/pagina-obrigado.md +55 -0
- package/extracted-skill/tracking-events-generator/models/pinterest/conversions-api-template.js +144 -0
- package/extracted-skill/tracking-events-generator/models/pinterest/event-mappings.json +48 -0
- package/extracted-skill/tracking-events-generator/models/pinterest/tag-template.js +28 -0
- package/extracted-skill/tracking-events-generator/models/quiz-funnel.md +132 -0
- package/extracted-skill/tracking-events-generator/models/reddit/conversions-api-template.js +205 -0
- package/extracted-skill/tracking-events-generator/models/reddit/event-mappings.json +56 -0
- package/extracted-skill/tracking-events-generator/models/reddit/pixel-template.js +19 -0
- package/extracted-skill/tracking-events-generator/models/scenarios/behavior-engine.js +425 -0
- package/extracted-skill/tracking-events-generator/models/scenarios/real-estate-logic.md +50 -0
- package/extracted-skill/tracking-events-generator/models/scenarios/sales-page-logic.md +50 -0
- package/extracted-skill/tracking-events-generator/models/trafego-direto.md +582 -0
- package/extracted-skill/tracking-events-generator/models/webinar-registration.md +63 -0
- package/extracted-skill/tracking-events-generator/route-intent-capture.js +222 -0
- package/extracted-skill/tracking-events-generator/tracking.config.js +46 -0
- package/extracted-skill/tracking-events-generator/walkthrough.md +26 -0
- package/package.json +89 -0
- package/scripts/build-sdk.js +106 -0
- package/server-edge-tracker/.client.env.example +14 -0
- package/server-edge-tracker/INSTALAR.md +527 -0
- package/server-edge-tracker/SEGMENTATION-DOCS.md +513 -0
- package/server-edge-tracker/config/utm-mapping.json +64 -0
- package/server-edge-tracker/deploy-client.cjs +76 -0
- package/server-edge-tracker/index.ts +1164 -0
- package/server-edge-tracker/migrate-new-db.sql +137 -0
- package/server-edge-tracker/migrate-v2.sql +16 -0
- package/server-edge-tracker/migrate-v3.sql +6 -0
- package/server-edge-tracker/migrate-v4.sql +18 -0
- package/server-edge-tracker/migrate-v5.sql +17 -0
- package/server-edge-tracker/migrate-v6.sql +24 -0
- package/server-edge-tracker/migrate-v7.sql +64 -0
- package/server-edge-tracker/migrate.sql +111 -0
- package/server-edge-tracker/modules/db.ts +702 -0
- package/server-edge-tracker/modules/dispatch/ga4.ts +72 -0
- package/server-edge-tracker/modules/dispatch/meta.ts +143 -0
- package/server-edge-tracker/modules/dispatch/platforms.ts +255 -0
- package/server-edge-tracker/modules/dispatch/tiktok.ts +107 -0
- package/server-edge-tracker/modules/dispatch/whatsapp.ts +279 -0
- package/server-edge-tracker/modules/intelligence.ts +589 -0
- package/server-edge-tracker/modules/ml/bidding.ts +247 -0
- package/server-edge-tracker/modules/ml/fraud.ts +302 -0
- package/server-edge-tracker/modules/ml/logistic.ts +226 -0
- package/server-edge-tracker/modules/ml/ltv.ts +531 -0
- package/server-edge-tracker/modules/ml/matchquality.ts +232 -0
- package/server-edge-tracker/modules/ml/quiz.ts +343 -0
- package/server-edge-tracker/modules/ml/roas.ts +255 -0
- package/server-edge-tracker/modules/ml/segmentation.ts +407 -0
- package/server-edge-tracker/modules/nurture.ts +257 -0
- package/server-edge-tracker/modules/utils.ts +311 -0
- package/server-edge-tracker/modules/utm/utm-enricher.ts +231 -0
- package/server-edge-tracker/schema-ab-ltv.sql +97 -0
- package/server-edge-tracker/schema-bidding.sql +86 -0
- package/server-edge-tracker/schema-fraud.sql +90 -0
- package/server-edge-tracker/schema-indexes.sql +67 -0
- package/server-edge-tracker/schema-ltv-feedback.sql +11 -0
- package/server-edge-tracker/schema-quiz.sql +52 -0
- package/server-edge-tracker/schema-sales-engine.sql +113 -0
- package/server-edge-tracker/schema-segmentation.sql +219 -0
- package/server-edge-tracker/schema-utm.sql +82 -0
- package/server-edge-tracker/schema.sql +265 -0
- package/server-edge-tracker/types.ts +258 -0
- package/server-edge-tracker/wrangler.toml +136 -0
- package/templates/afiliado-sem-landing.md +312 -0
- package/templates/captura-de-lead.md +78 -0
- package/templates/captura-lead-evento-externo.md +99 -0
- package/templates/checkout-proprio.md +111 -0
- package/templates/install/.claude/commands/cdp.md +1 -0
- package/templates/install/CLAUDE.md +65 -0
- package/templates/lancamento-imobiliario.md +344 -0
- package/templates/linkedin/tag-template.js +46 -0
- package/templates/multi-step-checkout.md +672 -0
- package/templates/pagina-obrigado.md +55 -0
- package/templates/pinterest/conversions-api-template.js +144 -0
- package/templates/pinterest/event-mappings.json +48 -0
- package/templates/pinterest/tag-template.js +28 -0
- package/templates/quiz-funnel.md +132 -0
- package/templates/reddit/conversions-api-template.js +205 -0
- package/templates/reddit/event-mappings.json +56 -0
- package/templates/reddit/pixel-template.js +19 -0
- package/templates/scenarios/behavior-engine.js +425 -0
- package/templates/scenarios/real-estate-logic.md +50 -0
- package/templates/scenarios/sales-page-logic.md +50 -0
- package/templates/spotify/pixel-template.js +46 -0
- package/templates/trafego-direto.md +582 -0
- package/templates/vsl-page.md +292 -0
- 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
|
+
}
|