Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.joinrefine.io/llms.txt

Use this file to discover all available pages before exploring further.

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

surface
string
required
Where products are displayed. Common values: search_results, product_page, home_page, category_page, cart_page.
source
string
required
How products were generated. Values: text-search, image-search, similar-items, visitor-recs, user-recs, plp, curated.
itemIds
string[]
required
Array of product IDs being served.
query
string
The search query, if applicable.
totalResults
number
Total available results before pagination.
metadata
object
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