Skip to main content

Analytics

Dukanext has a built-in analytics system. Emitting events from your theme populates the merchant's analytics dashboard with visitor data, conversion metrics, and product performance.

Implementing analytics is strongly recommended. It is not enforced by the API, but merchants rely on this data to understand their store performance. A theme without analytics gives them a blind spot.

If you prefer to use Google Analytics or another provider instead, that is fine — just make sure the merchant knows.

The event endpoint

PUT /access/api/v1/analytics

Fire-and-forget. Do not await this or block rendering on it. Use keepalive: true so events survive page navigation.

fetch(`${BASE_URL}/analytics`, {
method: 'PUT',
keepalive: true,
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY,
},
body: JSON.stringify(payload),
}).catch(() => {}); // never let analytics errors surface to the user

Session and user IDs

Two IDs are required on every event. Generate them once and persist them.

userId — identifies the visitor across sessions. Generate once and store in localStorage indefinitely.

function getUserId() {
const KEY = 'dn_user_id';
let id = localStorage.getItem(KEY);
if (!id) {
id = `${crypto.randomUUID()}-${Date.now()}`;
localStorage.setItem(KEY, id);
}
return id;
}

sessionId — identifies a single browsing session. Expires after 30 minutes of inactivity. Renew the TTL on each event so active sessions never expire mid-browse.

function getSessionId(currentPath) {
const KEY = 'dn_session_id';
const TTL = 30 * 60 * 1000;
const now = Date.now();

let stored = JSON.parse(localStorage.getItem(KEY) || 'null');

if (stored?.expiry && now < stored.expiry) {
// Renew TTL on activity
localStorage.setItem(KEY, JSON.stringify({ ...stored, expiry: now + TTL }));
return { sessionId: stored.value, sessionStartPage: stored.sessionStartPage };
}

const sessionId = `SESSION-${crypto.randomUUID()}-${Date.now()}`;
localStorage.setItem(KEY, JSON.stringify({
value: sessionId,
expiry: now + TTL,
sessionStartPage: currentPath,
}));
return { sessionId, sessionStartPage: currentPath };
}

Event payload

{
"userId": "550e8400-e29b-41d4-a716-446655440000-1759904642100",
"sessionId": "SESSION-b0b13d41-7651-4ec3-87e2-77a6dc0aa60e-1770181330638",
"sessionStartPage": "/",
"referrerUrl": "https://google.com",
"hasCustomerAccount": false,
"customersId": null,
"event": "page_view",
"metadata": { "url": "/shop/running-shoes" }
}

referrerUrl, hasCustomerAccount, and customersId are optional but improve the data quality.

Events

EventWhen to emitMetadata
page_viewEvery page load{ url: currentPath }
product_viewProduct detail page load{ productId, url }
add_to_cartItem added to cart{ productId, quantity }
remove_from_cartItem removed from cart{ productId }
begin_checkoutCustomer enters checkout{}
complete_checkoutOrder placed successfully{ orderId }
searchSearch performed{ term }
add_to_wishlistItem added to wishlist{ productId }
social_referralPage load with utm_source in URL{ utm_source, utm_medium, utm_campaign }

Deduplication

page_view, product_view, and social_referral should only fire once per unique context per session. Track fired events in a module-level Set (outside React, outside component lifecycle) so they survive re-renders and StrictMode double-effects:

const firedEvents = new Set();

function shouldFire(event, key) {
const deduped = ['page_view', 'product_view', 'social_referral'];
if (!deduped.includes(event)) return true;
if (firedEvents.has(key)) return false;
firedEvents.add(key);
return true;
}

// Example keys:
// page_view: `${sessionId}:page_view:${url}`
// product_view: `${sessionId}:product_view:${productId}`

Gate analytics behind shop health

Do not emit events until the health check has passed and the shop ID is confirmed. The shopId from the health response is required for the event to be attributed correctly.

// Only fire if shop is loaded
if (!shopId) return;
emitEvent({ event: 'page_view', data: { url: pathname }, shopId });