Overview
Event tracking captures user interactions—impressions, clicks, views, adds-to-cart, and purchases. This data powers:
- Analytics: Understand how users interact with search and recommendations
- Attribution: Link sales back to the search or recommendation that drove them
- Model Improvement: Feedback loops that improve recommendations over time
The ServeContext Pattern
When products are displayed, you create a ServeContext that tracks subsequent interactions:
// Products are served
const context = refine.events.trackSearch(query, results, options);
// User interacts
context.trackClick(productId, position); // They clicked
context.trackView(productId, position); // It became visible
context.trackAddToCart(productId); // They added to cart
This pattern ensures all interactions are correctly attributed to the original serve event.
Quick Start
import { Refine } from '@refine-ai/sdk';
const refine = new Refine({
apiKey: process.env.REFINE_API_KEY,
organizationId: 'org_abc123',
catalogId: 'cat_xyz789'
});
// Search and track
const results = await refine.search.text({ query: 'shoes', topK: 20 });
const searchContext = refine.events.trackSearch(
'shoes',
results.results,
{ surface: 'search_results', totalResults: results.totalResults }
);
// Track clicks
function handleProductClick(productId: string, position: number) {
searchContext.trackClick(productId, position);
}
// Track purchase (separate from serve context)
refine.events.trackPurchase({
orderId: 'order_123',
value: 149.99,
currency: 'USD',
items: [{ itemId: 'sku_001', quantity: 1, unitPrice: 149.99 }]
});
Event Types
| Event | Method | When to Track |
|---|
| Items Served | trackItemsServed() / trackSearch() / trackRecommendations() | Products displayed to user |
| Click | context.trackClick() | User clicks a product |
| View | context.trackView() | Product becomes visible (50%+ in viewport for 1s) |
| Add to Cart | context.trackAddToCart() | User adds product to cart |
| Purchase | trackPurchase() | Order completed |
Tracking Methods
trackSearch()
Convenience method for search results:
const searchContext = refine.events.trackSearch(
query, // The search query
results.results, // Array of products
{
surface: 'search_results',
totalResults: results.totalResults
}
);
trackRecommendations()
Convenience method for recommendation results:
const recsContext = refine.events.trackRecommendations(
recs.serveId, // From recommendation response
recs.results, // Array of products
'product_page', // Surface
'similar-items', // Source
{ anchorId: 'sku_001' } // Optional metadata
);
trackItemsServed()
Low-level method for custom scenarios:
const context = refine.events.trackItemsServed({
surface: 'home_page',
source: 'curated',
itemIds: ['sku_001', 'sku_002', 'sku_003'],
totalResults: 3
});
trackPurchase()
Track completed purchases:
refine.events.trackPurchase({
orderId: 'order_12345',
value: 299.98,
currency: 'USD',
items: [
{ itemId: 'sku_001', quantity: 1, unitPrice: 199.99 },
{ itemId: 'sku_002', quantity: 2, unitPrice: 49.99 }
]
});
ServeContext Methods
After calling a track method, you get a context with these methods:
const context = refine.events.trackSearch(query, results, options);
// User clicked product at position 3
context.trackClick('sku_001', 3);
// Product became visible at position 0
context.trackView('sku_001', 0);
// User added product to cart
context.trackAddToCart('sku_001');
trackClick(itemId, position)
Track when a user clicks on a product. position is the 0-indexed position in the results.
trackView(itemId, position)
Track when a product becomes visible. Call when 50%+ of the product is visible for 1+ second.
Track when a user adds a product to their cart from this serve context.
Event Queue Management
Events are automatically batched and sent. You can control this behavior:
// Force send all queued events immediately
await refine.events.flush();
// Pause event sending (e.g., when offline)
refine.events.pause();
// Resume event sending
refine.events.resume();
// Get queue metrics
const metrics = refine.events.getMetrics();
console.log(metrics);
// { queued: 5, sent: 142, failed: 0, retrying: 0 }
Configuration
Control batching, persistence, and retries in SDK initialization:
const refine = new Refine({
apiKey: process.env.REFINE_API_KEY,
organizationId: 'org_abc123',
catalogId: 'cat_xyz789',
events: {
enabled: true,
batchSize: 10, // Events per batch
flushInterval: 5000, // Max ms between sends
maxQueueSize: 1000, // Max events to queue
maxRetries: 3, // Retry attempts
persistence: 'localStorage', // Queue storage
sessionTimeout: 30 * 60 * 1000 // Session length
}
});
Page Unload Handling
Ensure events are sent before the user leaves:
// Modern approach (recommended)
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
refine.events.flush();
}
});
// Fallback for older browsers
window.addEventListener('beforeunload', () => {
refine.events.flush();
});
Complete Example
import { Refine } from '@refine-ai/sdk';
const refine = new Refine({
apiKey: process.env.REFINE_API_KEY,
organizationId: 'org_abc123',
catalogId: 'cat_xyz789'
});
class SearchPage {
private searchContext: any;
async search(query: string) {
const results = await refine.search.text({ query, topK: 24 });
// Track the search
this.searchContext = refine.events.trackSearch(query, results.results, {
surface: 'search_results',
totalResults: results.totalResults
});
this.renderResults(results.results);
this.setupViewabilityTracking();
return results;
}
private renderResults(products: any[]) {
const container = document.getElementById('results')!;
container.innerHTML = products.map((product, index) => `
<div class="product" data-id="${product.productId}" data-position="${index}">
<img src="${product.imageUrl}" alt="${product.title}" />
<h3>${product.title}</h3>
<p>$${product.price}</p>
<button class="add-to-cart">Add to Cart</button>
</div>
`).join('');
// Click handlers
container.querySelectorAll('.product').forEach(el => {
const productId = (el as HTMLElement).dataset.id!;
const position = parseInt((el as HTMLElement).dataset.position!);
el.addEventListener('click', (e) => {
if ((e.target as HTMLElement).classList.contains('add-to-cart')) {
this.searchContext?.trackAddToCart(productId);
} else {
this.searchContext?.trackClick(productId, position);
}
});
});
}
private setupViewabilityTracking() {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const el = entry.target as HTMLElement;
const productId = el.dataset.id!;
const position = parseInt(el.dataset.position!);
// Track view after 1 second of visibility
setTimeout(() => {
if (entry.isIntersecting) {
this.searchContext?.trackView(productId, position);
observer.unobserve(el);
}
}, 1000);
}
});
},
{ threshold: 0.5 }
);
document.querySelectorAll('.product').forEach(el => {
observer.observe(el);
});
}
}
Next Steps