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
| Event | When to emit | Metadata |
|---|---|---|
page_view | Every page load | { url: currentPath } |
product_view | Product detail page load | { productId, url } |
add_to_cart | Item added to cart | { productId, quantity } |
remove_from_cart | Item removed from cart | { productId } |
begin_checkout | Customer enters checkout | {} |
complete_checkout | Order placed successfully | { orderId } |
search | Search performed | { term } |
add_to_wishlist | Item added to wishlist | { productId } |
social_referral | Page 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 });