Recipe: Tracking de conversiones end-to-end
Para que Neuroon atribuya una venta a la búsqueda que la originó hay que enlazar dos eventos: el click del usuario sobre un resultado del widget y la conversión final del pedido. El widget hace la primera mitad sólo. Tu servidor tiene que cerrar el ciclo.
Cómo funciona la atribución
- El usuario busca en el widget y hace click en un resultado.
- El widget escribe un cookie en el navegador (
neuroon_clicks) con un mapproductId → searchLogIdpara cada producto sobre el que el usuario haya hecho click. - El usuario va a tu checkout y termina el pedido.
- Tu servidor lee el cookie en el handler de "pedido confirmado", construye el array
conversions[]añadiendo elsearchLogIdcorrespondiente a cada line item, y llama aPOST /api/plugin/shops/{shopId}/track/conversion. - Neuroon valida la firma del
searchLogId, detecta los productos atribuibles y suma sulineTotalal revenue atribuido a Neuroon.
Sin searchLogId en el payload, Neuroon no puede atribuir la conversión. El item queda registrado como ruido.
El cookie neuroon_clicks
| Campo | Valor |
|---|---|
| Nombre | neuroon_clicks |
| Path | / |
SameSite | Lax |
Secure | true (en HTTPS) |
HttpOnly | false (necesario para que el server lo lea en el next request) |
| Tamaño máx | 4096 bytes (truncado si lo supera) |
| Formato | JSON: { "<productId>": "<searchLogId>", "<productId>": "<searchLogId>", … } |
| TTL | Durante la sesión del usuario; el widget renueva entradas con cada click |
Las claves del JSON son externalId del producto (el mismo que mandaste en /products/sync). Cada searchLogId es una cadena opaca emitida por Neuroon en la respuesta de búsqueda.
Si el
productIddel item del pedido no aparece en el cookie, ese item no se atribuye — el usuario llegó a comprarlo por otro canal (URL directa, otro buscador, navigation interna). Es el comportamiento correcto.
El endpoint
/api/plugin/shops/{shopId}/track/conversionPOST /api/plugin/shops/shop_xxxxxxxx/track/conversion HTTP/1.1
Host: api.neuroon.ai
X-Shop-API-Key: sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Origin: https://your-shop.example
Content-Type: application/json
{
"orderId": "ORD-2026-0001",
"orderValue": 199.95,
"attributedValue": 79.98,
"currency": "EUR",
"conversions": [
{ "productId": "SKU-001", "searchLogId": "slog_abc123", "quantity": 2, "lineTotal": 79.98 }
]
}
Campos del payload:
| Campo | Requerido | Descripción |
|---|---|---|
orderId | sí | Identificador único de tu pedido. Idempotency key — reenviar no duplica. |
orderValue | sí | Total del pedido (todos los items). |
attributedValue | recomendado | Suma de lineTotal de items que tienen searchLogId. Si no lo mandas, Neuroon lo deriva de conversions[]. |
currency | sí | ISO 4217 (EUR, USD…). |
conversions[] | sí | Lista de items con atribución. Items sin searchLogId deben omitirse del array. |
conversions[].productId | sí | externalId del producto (mismo que en /products/sync). |
conversions[].searchLogId | sí | Valor obtenido del cookie. Sin esto la atribución no funciona. |
conversions[].quantity | sí | Unidades vendidas del item. |
conversions[].lineTotal | sí | Subtotal del item (unitPrice × quantity). |
Idempotencia: Neuroon deduplica por orderId. Reenviar la misma conversión no infla métricas.
Por qué la conversión va server-side
Confiar solo en un pixel JS pierde una parte significativa de las conversiones:
| Bloqueo | Impacto típico |
|---|---|
| Adblockers (uBlock, Ghostery, Brave Shields) | 25-40 % en mercados europeos |
| Safari ITP + iOS Private Relay | impacto residual sobre cookies |
| CSPs estrictas (banca, gobierno) | bloquean cualquier pixel third-party |
| Service workers offline | no envían beacons antes de cerrar tab |
Llamando al endpoint server-to-server desde el handler que confirma el pedido eliminas esos cuatro vectores. Además, server-side puedes mandar el lineTotal real del pedido (con descuentos aplicados), no la estimación que tendría el JS del navegador.
Implementaciones
import fetch from 'node-fetch';
const COOKIE_NAME = 'neuroon_clicks';
// Lee el cookie del request entrante (Express/Fastify/Next.js Route Handler).
function readClicksCookie(rawCookieHeader) {
if (!rawCookieHeader) return {};
const match = rawCookieHeader
.split(';')
.map(c => c.trim())
.find(c => c.startsWith(COOKIE_NAME + '='));
if (!match) return {};
try {
return JSON.parse(decodeURIComponent(match.split('=')[1]));
} catch {
return {};
}
}
export async function trackConversion(order, rawCookieHeader) {
const clicks = readClicksCookie(rawCookieHeader);
const conversions = order.lines
.map(l => {
const searchLogId = clicks[String(l.sku)];
if (!searchLogId) return null; // sin atribución → omitir
return {
productId: String(l.sku),
searchLogId,
quantity: Number(l.quantity),
lineTotal: Number(l.unitPrice * l.quantity),
};
})
.filter(Boolean);
const attributedValue = conversions
.reduce((sum, c) => sum + c.lineTotal, 0);
const payload = {
orderId: String(order.id),
orderValue: Number(order.total),
attributedValue,
currency: order.currency,
conversions,
};
try {
const r = await fetch(
`${process.env.NEUROON_API_URL}/api/plugin/shops/${process.env.NEUROON_SHOP_ID}/track/conversion`,
{
method: 'POST',
headers: {
'X-Shop-API-Key': process.env.NEUROON_API_KEY,
'Origin': process.env.NEUROON_ORIGIN,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
},
);
if (!r.ok) console.warn('Neuroon conversion tracking failed', r.status);
} catch (err) {
console.warn('Neuroon conversion tracking error', err);
}
}
// Uso desde tu handler de pedido confirmado
export async function onOrderConfirmed(req, order) {
await markOrderAsPaid(order.id);
trackConversion(order, req.headers.cookie); // fire-and-forget
}Patrón crítico: pasarelas de pago cross-origin
Si tu checkout redirige a una pasarela externa (PayPal, Stripe 3DS, Klarna, Redsys…) y el usuario vuelve por POST a tu dominio, el cookie neuroon_clicks puede perderse en el redirect porque tiene SameSite=Lax. Sin cookie, no hay atribución.
Solución: persiste los clicks en almacenamiento server-side antes del redirect a la pasarela, y léelos del almacenamiento si el cookie no llega de vuelta. WooCommerce lo hace automáticamente vía order meta. Para custom:
// Antes de redirigir al gateway
async function persistClicksBeforeGateway(req, orderId) {
const clicks = readClicksCookie(req.headers.cookie);
if (Object.keys(clicks).length > 0) {
await db.query(
'UPDATE orders SET neuroon_clicks_raw = $1 WHERE id = $2',
[JSON.stringify(clicks), orderId],
);
}
}
// Al confirmar el pedido (después del callback del gateway)
async function getClicksWithFallback(req, orderId) {
const fromCookie = readClicksCookie(req.headers.cookie);
if (Object.keys(fromCookie).length > 0) return fromCookie;
// Fallback: lee de la BD lo que persistimos antes del redirect
const row = await db.query(
'SELECT neuroon_clicks_raw FROM orders WHERE id = $1',
[orderId],
);
return row?.neuroon_clicks_raw ? JSON.parse(row.neuroon_clicks_raw) : {};
}
Borra
neuroon_clicks_rawde la BD inmediatamente tras leerlo. Es un dato temporal consearchLogIdopacos — no debe persistir más allá del flujo de tracking.
Outbox para reintentos
Si tu integración no puede tolerar pérdida de eventos, persiste el payload en una tabla outbox y reintenta con un job:
CREATE TABLE neuroon_conversion_outbox (
order_id VARCHAR(64) PRIMARY KEY,
payload JSON NOT NULL,
attempts INT NOT NULL DEFAULT 0,
last_error VARCHAR(1024),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
synced_at TIMESTAMP NULL
);
Job que procesa cada N minutos con backoff exponencial (típicamente 5 / 10 / 20 min):
async function flushOutbox() {
const rows = await db.query(`
SELECT * FROM neuroon_conversion_outbox
WHERE synced_at IS NULL AND attempts < 5
ORDER BY created_at LIMIT 100`);
for (const row of rows) {
try {
await postConversion(row.payload);
await db.query(`UPDATE neuroon_conversion_outbox SET synced_at = NOW() WHERE order_id = $1`, [row.order_id]);
} catch (err) {
await db.query(`
UPDATE neuroon_conversion_outbox
SET attempts = attempts + 1, last_error = $1
WHERE order_id = $2`, [err.message, row.order_id]);
}
}
}
Validar la atribución
- Abre tu storefront en una pestaña nueva (no incógnito).
- Busca un producto en el widget y haz click en un resultado.
- Abre DevTools → Application → Cookies →
neuroon_clicks. Debes ver el JSON con tuproductId. - Completa la compra de ese producto.
- En el dashboard de Neuroon (sección Conversiones), debe aparecer el pedido en cuestión de segundos con
attributedValueigual allineTotaldel item.
Si el pedido aparece pero con attributedValue: 0, revisa que el productId del item del pedido es exactamente el mismo string que el externalId con el que sincronizaste el producto.
Errores frecuentes
| Síntoma | Causa | Solución |
|---|---|---|
400 con orderValue must be >= 0 | orderValue negativo (refunds) | Las refunds requieren un endpoint distinto. |
Conversión registrada pero attributedValue: 0 | Items del pedido sin match en el cookie (URL directa, otro canal) | Comportamiento correcto — no todos los pedidos vienen de búsqueda. |
| Conversión no aparece en dashboard | Origin no coincide o shopId cruzado de entornos | Verifica Origin y que la API Key sea del entorno correcto. |
| Conversiones duplicadas | Reenvíos sin idempotency key estable | Usa siempre el mismo orderId por pedido (no timestamps). |
| Atribución cae a 0 desde que añadiste PayPal/3DS | SameSite=Lax pierde el cookie en redirect cross-origin | Implementa el patrón de persistencia pre-redirect (sección anterior). |
productId del cookie no matchea con el del pedido | Sync usa SKU-001 y el pedido usa wc_post_42 | Mantén un único externalId por producto: el mismo en /products/sync y en el cookie. |
Próximos pasos
plugins/wordpress/admin-dashboard— visualización de conversiones en panel.- API ·
POST /track/conversion— referencia del endpoint. - Recipe · Custom cart bridge — eventos del carrito que el widget consume para personalización.