Saltar al contenido principal

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

  1. El usuario busca en el widget y hace click en un resultado.
  2. El widget escribe un cookie en el navegador (neuroon_clicks) con un map productId → searchLogId para cada producto sobre el que el usuario haya hecho click.
  3. El usuario va a tu checkout y termina el pedido.
  4. Tu servidor lee el cookie en el handler de "pedido confirmado", construye el array conversions[] añadiendo el searchLogId correspondiente a cada line item, y llama a POST /api/plugin/shops/{shopId}/track/conversion.
  5. Neuroon valida la firma del searchLogId, detecta los productos atribuibles y suma su lineTotal al revenue atribuido a Neuroon.

Sin searchLogId en el payload, Neuroon no puede atribuir la conversión. El item queda registrado como ruido.

CampoValor
Nombreneuroon_clicks
Path/
SameSiteLax
Securetrue (en HTTPS)
HttpOnlyfalse (necesario para que el server lo lea en el next request)
Tamaño máx4096 bytes (truncado si lo supera)
FormatoJSON: { "<productId>": "<searchLogId>", "<productId>": "<searchLogId>", … }
TTLDurante 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 productId del 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

POST/api/plugin/shops/{shopId}/track/conversion
POST /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:

CampoRequeridoDescripción
orderIdIdentificador único de tu pedido. Idempotency key — reenviar no duplica.
orderValueTotal del pedido (todos los items).
attributedValuerecomendadoSuma de lineTotal de items que tienen searchLogId. Si no lo mandas, Neuroon lo deriva de conversions[].
currencyISO 4217 (EUR, USD…).
conversions[]Lista de items con atribución. Items sin searchLogId deben omitirse del array.
conversions[].productIdexternalId del producto (mismo que en /products/sync).
conversions[].searchLogIdValor obtenido del cookie. Sin esto la atribución no funciona.
conversions[].quantityUnidades vendidas del item.
conversions[].lineTotalSubtotal 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:

BloqueoImpacto típico
Adblockers (uBlock, Ghostery, Brave Shields)25-40 % en mercados europeos
Safari ITP + iOS Private Relayimpacto residual sobre cookies
CSPs estrictas (banca, gobierno)bloquean cualquier pixel third-party
Service workers offlineno 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

track-conversion.js
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_raw de la BD inmediatamente tras leerlo. Es un dato temporal con searchLogId opacos — 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

  1. Abre tu storefront en una pestaña nueva (no incógnito).
  2. Busca un producto en el widget y haz click en un resultado.
  3. Abre DevTools → Application → Cookies → neuroon_clicks. Debes ver el JSON con tu productId.
  4. Completa la compra de ese producto.
  5. En el dashboard de Neuroon (sección Conversiones), debe aparecer el pedido en cuestión de segundos con attributedValue igual al lineTotal del 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íntomaCausaSolución
400 con orderValue must be >= 0orderValue negativo (refunds)Las refunds requieren un endpoint distinto.
Conversión registrada pero attributedValue: 0Items 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 dashboardOrigin no coincide o shopId cruzado de entornosVerifica Origin y que la API Key sea del entorno correcto.
Conversiones duplicadasReenvíos sin idempotency key estableUsa siempre el mismo orderId por pedido (no timestamps).
Atribución cae a 0 desde que añadiste PayPal/3DSSameSite=Lax pierde el cookie en redirect cross-originImplementa el patrón de persistencia pre-redirect (sección anterior).
productId del cookie no matchea con el del pedidoSync usa SKU-001 y el pedido usa wc_post_42Mantén un único externalId por producto: el mismo en /products/sync y en el cookie.

Próximos pasos