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
Conversion Tracking Track purchases and attribution
Auto Track Plugin Automatic event tracking