Overview
Tracking events at each stage of the user journey—from seeing products to clicking them—provides the data needed for analytics and recommendation improvements.
Event Flow
Search/Recs Request → Items Served → User Sees (View) → User Clicks → Add to Cart → Purchase
Each event builds on the previous, creating a complete picture of user behavior.
Tracking Search Results
Basic Implementation
const results = await refine.search.text({ query: 'summer dress', topK: 24 });
const searchContext = refine.events.trackSearch(
'summer dress',
results.results,
{
surface: 'search_results',
totalResults: results.totalResults
}
);
Full Parameters
const searchContext = refine.events.trackSearch(
query: string,
products: SearchResultItem[],
options: {
surface: string; // Where products are displayed
totalResults: number; // Total matching products
filters?: Filter[]; // Applied filters
sortBy?: SortBy; // Applied sorting
}
);
Tracking Recommendations
const recs = await refine.recs.similarItems({
anchorId: 'sku_001',
topK: 8
});
const recsContext = refine.events.trackRecommendations(
recs.serveId,
recs.results,
'product_page', // surface
'similar-items', // source
{ anchorId: 'sku_001' } // metadata
);
Tracking Clicks
Track when a user clicks on a product:
// Basic click tracking
searchContext.trackClick(productId, position);
// Example with event handler
document.querySelectorAll('.product').forEach((el, index) => {
el.addEventListener('click', () => {
const productId = el.dataset.id;
searchContext.trackClick(productId, index);
});
});
The position parameter is 0-indexed and represents where the product appeared in the results. This is crucial for understanding if users click items at position 1 vs position 20.
Tracking Views (Viewability)
Track when a product becomes visible to the user. Standard viewability requires:
- At least 50% of the product visible
- Visible for at least 1 second
Using Intersection Observer
function setupViewabilityTracking(context: ServeContext) {
const viewedProducts = new Set<string>();
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
const el = entry.target as HTMLElement;
const productId = el.dataset.id!;
const position = parseInt(el.dataset.position!);
if (entry.isIntersecting && !viewedProducts.has(productId)) {
// Start 1-second timer
const timeout = setTimeout(() => {
// Check if still visible
const rect = el.getBoundingClientRect();
const isStillVisible = (
rect.top < window.innerHeight &&
rect.bottom > 0
);
if (isStillVisible) {
context.trackView(productId, position);
viewedProducts.add(productId);
observer.unobserve(el);
}
}, 1000);
// Store timeout to clear if element leaves viewport
el.dataset.viewTimeout = String(timeout);
} else if (!entry.isIntersecting) {
// Clear timeout if element leaves viewport before 1s
const timeout = el.dataset.viewTimeout;
if (timeout) {
clearTimeout(parseInt(timeout));
}
}
});
},
{ threshold: 0.5 }
);
document.querySelectorAll('[data-id]').forEach(el => {
observer.observe(el);
});
return observer;
}
Using Auto Track Plugin
For simpler implementation, use the AutoTrackPlugin:
import { AutoTrackPlugin } from '@refine-ai/sdk';
refine.use(new AutoTrackPlugin({
viewability: {
enabled: true,
threshold: 0.5, // 50% visible
duration: 1000 // 1 second
}
}));
See Auto Track Plugin for details.
Tracking Add to Cart
Track when a user adds a product to their cart:
// From within a serve context
searchContext.trackAddToCart(productId);
// Example implementation
document.querySelectorAll('.add-to-cart-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const productId = btn.closest('[data-id]').dataset.id;
// Track the event
searchContext.trackAddToCart(productId);
// Actually add to cart
addToCart(productId);
});
});
Low-Level Tracking
For custom scenarios, use trackItemsServed directly:
const context = refine.events.trackItemsServed({
surface: 'custom_carousel',
source: 'editorial-picks',
itemIds: ['sku_001', 'sku_002', 'sku_003'],
query: null,
totalResults: 3,
metadata: {
carouselName: 'Summer Collection',
placement: 'homepage-hero'
}
});
Parameters
Where products are displayed. Common values: search_results, product_page, home_page, category_page, cart_page.
How products were generated. Values: text-search, image-search, similar-items, visitor-recs, user-recs, plp, curated.
Array of product IDs being served.
The search query, if applicable.
Total available results before pagination.
Custom key-value pairs for additional context.
Complete Page Implementation
import { Refine } from '@refine-ai/sdk';
const refine = new Refine({
apiKey: process.env.REFINE_API_KEY,
organizationId: 'org_abc123',
catalogId: 'cat_xyz789'
});
class ProductGridTracker {
private context: any;
private observer: IntersectionObserver | null = null;
private viewedProducts = new Set<string>();
async loadSearchResults(query: string) {
const results = await refine.search.text({ query, topK: 24 });
this.context = refine.events.trackSearch(query, results.results, {
surface: 'search_results',
totalResults: results.totalResults
});
this.render(results.results);
this.setupTracking();
return results;
}
async loadRecommendations(anchorId: string) {
const recs = await refine.recs.similarItems({ anchorId, topK: 8 });
this.context = refine.events.trackRecommendations(
recs.serveId,
recs.results,
'product_page',
'similar-items',
{ anchorId }
);
this.render(recs.results);
this.setupTracking();
return recs;
}
private render(products: any[]) {
const container = document.getElementById('products')!;
container.innerHTML = products.map((p, i) => `
<article class="product-card" data-id="${p.productId}" data-position="${i}">
<img src="${p.imageUrl}" alt="${p.title}" loading="lazy" />
<h3>${p.title}</h3>
<p class="price">$${p.price.toFixed(2)}</p>
<button class="add-to-cart" aria-label="Add ${p.title} to cart">
Add to Cart
</button>
</article>
`).join('');
}
private setupTracking() {
this.setupClickTracking();
this.setupViewabilityTracking();
}
private setupClickTracking() {
document.querySelectorAll('.product-card').forEach(card => {
const productId = (card as HTMLElement).dataset.id!;
const position = parseInt((card as HTMLElement).dataset.position!);
// Product click
card.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
if (target.classList.contains('add-to-cart')) {
this.context?.trackAddToCart(productId);
this.handleAddToCart(productId);
} else {
this.context?.trackClick(productId, position);
this.navigateToProduct(productId);
}
});
});
}
private setupViewabilityTracking() {
// Clean up previous observer
this.observer?.disconnect();
this.viewedProducts.clear();
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const el = entry.target as HTMLElement;
const productId = el.dataset.id!;
const position = parseInt(el.dataset.position!);
if (this.viewedProducts.has(productId)) return;
// Delay to ensure genuine view
setTimeout(() => {
if (this.isElementVisible(el) && !this.viewedProducts.has(productId)) {
this.context?.trackView(productId, position);
this.viewedProducts.add(productId);
this.observer?.unobserve(el);
}
}, 1000);
});
},
{ threshold: 0.5 }
);
document.querySelectorAll('.product-card').forEach(card => {
this.observer!.observe(card);
});
}
private isElementVisible(el: Element): boolean {
const rect = el.getBoundingClientRect();
return rect.top < window.innerHeight && rect.bottom > 0;
}
private handleAddToCart(productId: string) {
// Your add to cart logic
}
private navigateToProduct(productId: string) {
window.location.href = `/products/${productId}`;
}
destroy() {
this.observer?.disconnect();
}
}
// Usage
const tracker = new ProductGridTracker();
await tracker.loadSearchResults('summer dress');
Best Practices
Do:
- Track clicks immediately on click, not on navigation complete
- Track views with a 1-second delay to filter accidental scrolls
- Track add-to-cart before the actual cart operation
- Use consistent surface and source values across your app
Don’t:
- Track views on render (before user could actually see them)
- Track clicks multiple times for the same interaction
- Forget to track interactions from keyboard navigation
- Block user interactions while waiting for tracking to complete
Next Steps