Saltar al contenido principal

DNN · Tracking de conversiones

Las conversiones son el evento más valioso del funnel y también el más vulnerable a adblockers, extensiones de privacidad y CSPs estrictas. La regla de oro:

Trackea conversiones server-side, no client-side.

El widget ya emite track:click automáticamente cuando un usuario interactúa con un resultado. La conversión final, en cambio, debes notificarla desde tu backend DNN cuando el pedido pasa a estado Confirmed / Paid.

Endpoint

POST/api/plugin/shops/{shopId}/track/conversion
{
"orderId": "ORD-2026-0001",
"orderValue": 199.95,
"currency": "EUR",
"conversions": [
{ "productId": "SKU-001", "searchLogId": "log_aaa", "quantity": 2, "lineTotal": 79.98 },
{ "productId": "SKU-042", "searchLogId": "log_bbb", "quantity": 1, "lineTotal": 119.97 }
]
}

searchLogId obligatorio (@NotBlank en ConversionItem, ShopRequestDTO.java:92). El widget lo emite en cada track:click y lo guarda en una cookie del comprador. En DNN tienes que capturarlo desde JS (cookie del navegador) y reenviarlo a tu servidor con cada línea de pedido. Sin searchLogId la API responde 400.

Headers:

X-Shop-API-Key: sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Origin: https://your-domain.example
Content-Type: application/json

Implementación con NeuroonClient

Si ya tienes el NeuroonClient montado (ver examples/full-csharp), una conversión es una línea:

public void OnOrderConfirmed(Order order)
{
var client = new NeuroonClient();

client.TrackConversionAsync(
orderId: order.Id.ToString(),
orderValue: order.Total,
currency: order.Currency,
lines: order.Lines.Select(l => new NeuroonClient.ConversionLine
{
ProductId = l.Sku,
Quantity = l.Quantity,
LineTotal = l.Price * l.Quantity
})
).GetAwaiter().GetResult();
}

Dónde enganchar el handler

Cada módulo de e-commerce DNN tiene su propio punto de extensión:

MóduloHook recomendado
DNN CommerceOrderController::ChangeOrderStatus cuando pasa a Paid.
NB_StoreNBrightBuyController::OrderEvent con eventType = "OrderConfirmed".
Hotcakes CommerceOrderService.OrderTransitioned con NewStatus = OrderStatus.Completed.
Módulo propioEl método donde marcas el pedido como cobrado / shipped.

Trackea una sola vez por pedido. Reenvíos no son destructivos (Neuroon deduplica por orderId), pero contaminan dashboards de retry.

Robustez vs. fallos

El tracking debe ser fire-and-forget: un error de Neuroon nunca debe bloquear la confirmación del pedido al cliente.

public async Task TrackOrderSafelyAsync(Order order)
{
try
{
await new NeuroonClient().TrackConversionAsync(
orderId: order.Id.ToString(),
orderValue: order.Total,
currency: order.Currency,
lines: order.Lines.Select(l => new NeuroonClient.ConversionLine
{
ProductId = l.Sku,
Quantity = l.Quantity,
LineTotal = l.Price * l.Quantity
}));
}
catch (Exception ex)
{
DnnLog.Warn($"Neuroon conversion tracking failed for order {order.Id}: {ex.Message}");
// No re-throw: la confirmación sigue su curso.
}
}

Reintentos

Polly ya está configurado dentro del cliente (3 intentos sobre 429 / 5xx). Si tras eso falla, persiste el pedido en una tabla local (Neuroon_Conversion_Outbox) y reintenta con un job nocturno. El endpoint es idempotente por orderId.

CREATE TABLE Neuroon_Conversion_Outbox (
OrderId NVARCHAR(64) PRIMARY KEY,
Payload NVARCHAR(MAX) NOT NULL,
Attempts INT NOT NULL DEFAULT 0,
LastError NVARCHAR(1024) NULL,
CreatedAt DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
UpdatedAt DATETIME2 NULL,
SyncedAt DATETIME2 NULL
);

Por qué NO confiar solo en el client-side

Bloqueo% aproximado de usuarios afectados
Adblockers (uBlock, Ghostery, Brave Shields)25-40 % en mercados europeos.
Safari ITP + iOS Private Relayimpacto residual sobre cookies third-party.
CSPs restrictivas (banca, gobierno)bloquean cualquier pixel third-party.
Service workers offlineno envían beacons antes de cerrar tab.

El tracking server-side hace que ninguno de estos casos te haga perder la conversión.

Rate limit

/track/conversion está dentro del bucket general de plugin endpoints. Para conversiones genuinas no llegarás al límite. Si tienes un volumen muy alto (> 10/s sostenido), agrupa en colas con outbox y suéltalas en bloques de 5/s.

Próximos pasos