feat: add Meta CAPI + Google Enhanced Conversions server-side tracking
- Add /api/conversions endpoint (Meta CAPI + GA4 Measurement Protocol) - SHA-256 PII hashing, event_id deduplication, fbp/fbc cookies - Client-side sendConversion() utility in PageShell.astro - Lead event tracking on form submit in home.js - GA4 allow_enhanced_conversions config
This commit is contained in:
124
server.js
124
server.js
@@ -1,13 +1,18 @@
|
||||
/**
|
||||
* MoreminiMore production server.
|
||||
*
|
||||
* Serves Astro static build (dist/) + handles POST /api/contact via Amazon SES.
|
||||
* Serves Astro static build (dist/) + handles POST /api/contact via Amazon SES
|
||||
* + POST /api/conversions for Meta CAPI + Google Enhanced Conversions.
|
||||
*
|
||||
* Env vars:
|
||||
* SES_ACCESS_KEY_ID — AWS IAM key with ses:SendEmail
|
||||
* SES_SECRET_ACCESS_KEY — AWS IAM secret
|
||||
* SES_REGION — default: ap-southeast-1
|
||||
* PORT — default: 4321
|
||||
* SES_ACCESS_KEY_ID — AWS IAM key with ses:SendEmail
|
||||
* SES_SECRET_ACCESS_KEY — AWS IAM secret
|
||||
* SES_REGION — default: ap-southeast-1
|
||||
* PORT — default: 4321
|
||||
* META_PIXEL_ID — default: 418349260078648
|
||||
* META_ACCESS_TOKEN — required for Meta CAPI
|
||||
* GA4_MEASUREMENT_ID — default: G-74BHREDLC3
|
||||
* GA4_API_SECRET — required for Google Enhanced Conversions
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
@@ -17,6 +22,7 @@ import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const PORT = parseInt(process.env.PORT || '4321', 10);
|
||||
@@ -146,6 +152,110 @@ app.post('/api/contact', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ── Meta CAPI + Google Enhanced Conversions ────────────────────────
|
||||
const META_PIXEL_ID = process.env.META_PIXEL_ID || '418349260078648';
|
||||
const META_ACCESS_TOKEN = process.env.META_ACCESS_TOKEN || '';
|
||||
const GA4_MEASUREMENT_ID = process.env.GA4_MEASUREMENT_ID || 'G-74BHREDLC3';
|
||||
const GA4_API_SECRET = process.env.GA4_API_SECRET || '';
|
||||
|
||||
function sha256(str) {
|
||||
if (!str) return null;
|
||||
return crypto.createHash('sha256')
|
||||
.update(String(str).trim().toLowerCase())
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
app.post('/api/conversions', async (req, res) => {
|
||||
const { event_name, event_id, event_time, event_source_url,
|
||||
user_data = {}, custom_data = {} } = req.body;
|
||||
|
||||
const results = { meta: null, google: null };
|
||||
|
||||
// ── Meta CAPI ──
|
||||
if (META_ACCESS_TOKEN) {
|
||||
try {
|
||||
const metaBody = {
|
||||
data: [{
|
||||
event_name,
|
||||
event_time,
|
||||
event_source_url,
|
||||
action_source: 'website',
|
||||
event_id,
|
||||
user_data: {
|
||||
em: sha256(user_data.em),
|
||||
ph: user_data.ph ? sha256(String(user_data.ph).replace(/[^0-9+]/g, '')) : null,
|
||||
fn: sha256(user_data.fn),
|
||||
ln: sha256(user_data.ln),
|
||||
fbp: user_data.fbp,
|
||||
fbc: user_data.fbc,
|
||||
client_ip_address: req.ip || req.headers['x-forwarded-for'] || req.socket.remoteAddress,
|
||||
client_user_agent: req.headers['user-agent'],
|
||||
},
|
||||
custom_data,
|
||||
}],
|
||||
};
|
||||
|
||||
const metaRes = await fetch(
|
||||
`https://graph.facebook.com/v22.0/${META_PIXEL_ID}/events?access_token=${META_ACCESS_TOKEN}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(metaBody),
|
||||
}
|
||||
);
|
||||
results.meta = await metaRes.json();
|
||||
console.info('[api/conversions] Meta CAPI:', JSON.stringify(results.meta));
|
||||
} catch (e) {
|
||||
console.error('[api/conversions] Meta CAPI error:', e);
|
||||
results.meta = { error: e.message };
|
||||
}
|
||||
} else {
|
||||
console.warn('[api/conversions] META_ACCESS_TOKEN not configured — skipping Meta CAPI');
|
||||
}
|
||||
|
||||
// ── Google Enhanced Conversions (Measurement Protocol) ──
|
||||
if (GA4_API_SECRET) {
|
||||
try {
|
||||
const gaBody = {
|
||||
client_id: user_data.fbp || crypto.randomUUID(),
|
||||
events: [{
|
||||
name: event_name.toLowerCase(),
|
||||
params: {
|
||||
engagement_time_msec: '100',
|
||||
session_id: event_id,
|
||||
...(custom_data || {}),
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
// Add user properties if available
|
||||
if (user_data.em || user_data.ph) {
|
||||
gaBody.user_properties = {};
|
||||
if (user_data.em) gaBody.user_properties.user_email = { value: sha256(user_data.em) };
|
||||
if (user_data.ph) gaBody.user_properties.user_phone = { value: sha256(String(user_data.ph).replace(/[^0-9+]/g, '')) };
|
||||
}
|
||||
|
||||
const gaRes = await fetch(
|
||||
`https://www.google-analytics.com/mp/collect?measurement_id=${GA4_MEASUREMENT_ID}&api_secret=${GA4_API_SECRET}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(gaBody),
|
||||
}
|
||||
);
|
||||
results.google = { status: gaRes.status, ok: gaRes.ok };
|
||||
console.info('[api/conversions] Google MP:', JSON.stringify(results.google));
|
||||
} catch (e) {
|
||||
console.error('[api/conversions] Google MP error:', e);
|
||||
results.google = { error: e.message };
|
||||
}
|
||||
} else {
|
||||
console.warn('[api/conversions] GA4_API_SECRET not configured — skipping Google Enhanced Conversions');
|
||||
}
|
||||
|
||||
res.json({ ok: true, results });
|
||||
});
|
||||
|
||||
// Static files
|
||||
if (existsSync(DIST)) {
|
||||
app.use(express.static(DIST, { maxAge: '1y', etag: true }));
|
||||
@@ -167,5 +277,7 @@ if (existsSync(DIST)) {
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`[server] Listening on http://0.0.0.0:${PORT}`);
|
||||
console.log(`[server] SES: ${sesConfigured ? 'configured ✓' : 'NOT configured (dev mode)'}`);
|
||||
console.log(`[server] Static: ${existsSync(DIST) ? `dist/ ✓` : 'dist/ NOT found'}`);
|
||||
console.log(`[server] Meta CAPI: ${META_ACCESS_TOKEN ? 'configured ✓' : 'NOT configured'}`);
|
||||
console.log(`[server] Google EC: ${GA4_API_SECRET ? 'configured ✓' : 'NOT configured'}`);
|
||||
console.log(`[server] Static: ${existsSync(DIST) ? 'dist/ ✓' : 'dist/ NOT found'}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user