Skip to main content

DNN · Conversion tracking

Conversions are the most valuable event in the funnel and also the most exposed to adblockers, privacy extensions and strict CSPs. The golden rule:

Track conversions server-side, not client-side.

The widget already emits track:click automatically when a user interacts with a result. The final conversion, however, must be reported from your DNN backend when the order moves to 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 is required (@NotBlank on ConversionItem, ShopRequestDTO.java:92). The widget emits it on every track:click and stores it in a cookie on the buyer's browser. On DNN you must capture it from JS (browser cookie) and forward it to your server with each order line. Without searchLogId the API responds 400.

Headers:

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

Implementation with NeuroonClient

If you already have the NeuroonClient set up (see examples/full-csharp), a conversion is a one-liner:

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();
}

Where to hook the handler

Each DNN e-commerce module has its own extension point:

ModuleRecommended hook
DNN CommerceOrderController::ChangeOrderStatus when it moves to Paid.
NB_StoreNBrightBuyController::OrderEvent with eventType = "OrderConfirmed".
Hotcakes CommerceOrderService.OrderTransitioned with NewStatus = OrderStatus.Completed.
Custom moduleThe method where you mark the order as paid / shipped.

Track only once per order. Resends are non-destructive (Neuroon deduplicates by orderId) but pollute retry dashboards.

Robustness against failures

Tracking must be fire-and-forget: a Neuroon error must never block order confirmation for the customer.

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: confirmation continues normally.
}
}

Retries

Polly is already configured inside the client (3 attempts on 429 / 5xx). If it still fails, persist the order in a local table (Neuroon_Conversion_Outbox) and retry from a nightly job. The endpoint is idempotent by 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
);

Why NOT trust client-side alone

BlockApproximate % of users affected
Adblockers (uBlock, Ghostery, Brave Shields)25-40 % in European markets.
Safari ITP + iOS Private Relayresidual impact on third-party cookies.
Strict CSPs (banking, government)block any third-party pixel.
Offline service workersdo not send beacons before tab close.

Server-side tracking ensures none of these cases costs you the conversion.

Rate limit

/track/conversion is within the general plugin endpoints bucket. For genuine conversions you will not hit the limit. If you have very high volume (> 10/s sustained), batch in queues with outbox and release in blocks of 5/s.

Next steps