> ## 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.

# Tracking Events

> Implement impression, click, and view tracking for search and recommendations

## 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

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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:

```typescript theme={null}
// 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);
  });
});
```

<Note>
  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.
</Note>

## 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

```typescript theme={null}
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:

```typescript theme={null}
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](/sdk/plugins/auto-track-plugin) for details.

## Tracking Add to Cart

Track when a user adds a product to their cart:

```typescript theme={null}
// 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:

```typescript theme={null}
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

<ParamField body="surface" type="string" required>
  Where products are displayed. Common values: `search_results`, `product_page`, `home_page`, `category_page`, `cart_page`.
</ParamField>

<ParamField body="source" type="string" required>
  How products were generated. Values: `text-search`, `image-search`, `similar-items`, `visitor-recs`, `user-recs`, `plp`, `curated`.
</ParamField>

<ParamField body="itemIds" type="string[]" required>
  Array of product IDs being served.
</ParamField>

<ParamField body="query" type="string">
  The search query, if applicable.
</ParamField>

<ParamField body="totalResults" type="number">
  Total available results before pagination.
</ParamField>

<ParamField body="metadata" type="object">
  Custom key-value pairs for additional context.
</ParamField>

## Complete Page Implementation

```typescript theme={null}
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

<Tip>
  **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
</Tip>

<Warning>
  **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
</Warning>

## Next Steps

<CardGroup cols={2}>
  <Card title="Conversion Tracking" icon="cart-shopping" href="/sdk/events/conversion-tracking">
    Track purchases and attribution
  </Card>

  <Card title="Auto Track Plugin" icon="wand-magic-sparkles" href="/sdk/plugins/auto-track-plugin">
    Automatic event tracking
  </Card>
</CardGroup>
