Recommendations API (avsb.recs)

Recommendations in A vs B are headless: the API returns plain product data and your page renders it. There is no widget, no injected markup, no layout options: your components, your design system, our data and measurement.

The same client is available in two places:

  • window.avsb.recs: for page code. Available on every page where the snippet runs.
  • options.recs: inside experiment variation code (a variation is one specific version being tested: the control or a challenger). Exactly the same get, trackClick, and trackView methods as window.avsb.recs, pre-bound to the running variation so every impression and click is attributed to that experiment and variation automatically. Use this one inside variations: attribution is ambient, zero extra input.
TypeScript
interface RecsClient {  get(req: RecsRequest): Promise<RecsResult>   // resolves items; never throws  trackClick(productId: string, opts?: { position?: number }): void  trackView(sku: string, meta?: { category?: string }): void}interface RecsRequest {  recipe: string                     // the recipe handle (its output-dataset slug)  context?: {    seed?: string                    // single seed product key (product page)    seeds?: string[]                 // up to 3 seed keys (cart page)    recentlyViewed?: number          // seed from the visitor's recently-viewed buffer (up to 3)  }  maxItems?: number                  // 1–24, default 8  excludeOutOfStock?: boolean        // defaults to the recipe's own setting  surface?: string                   // rec surface id for multi-surface pages, default 'default'}interface RecsResult {  items: RecProduct[]  served: boolean                    // true when the chain produced items  recipe: string  source: string | null              // slug of the chain step that served, null on a miss  fallbackStep: number | null        // 0 = primary, 1+ = fallback step, null on a miss  degraded: boolean                  // true when the lookup could not be made (timeout, network, or a snippet build with no shop features)}interface RecProduct {  id: string  title?: string  image?: string  href?: string  category?: string  price?: number                     // integer MINOR units (5999 = $59.99), resolved live  compareAtPrice?: number            // integer minor units, resolved live  currency?: string                  // ISO 4217 code for price / compareAtPrice  availability?: 'in_stock' | 'out_of_stock' | 'removed'}
TypeScript38 lines

The examples below hand rendering to page code of your own. These are the stand-ins they assume, so you can see what each one is expected to take:

TypeScript
declare function renderMyCard(item: RecProduct): HTMLElementdeclare function renderRecs(items: RecProduct[]): voiddeclare function getSeedSku(): stringdeclare function reportRecsOutage(recipe: string): void
TypeScript4 lines

avsb.recs.get

Plain text
avsb.recs.get(req: RecsRequest) => Promise<RecsResult>
Plain text1 line

Resolves recommendations for a recipe. The recipe handle is the recipe's output-dataset slug, shown on the recipe's page in the dashboard, and stable across re-runs. The full fallback chain, merchandising rules, and stock filter you configured on the recipe are all applied server-side. What you get back is the final, display-ready list.

TypeScript
const product = { sku: 'SKU-123' }const container = document.querySelector<HTMLElement>('.recs-grid')const result = await avsb.recs.get({  recipe: 'similar-products',  context: { seed: product.sku },  maxItems: 6,})if (container && result.served) {  container.replaceChildren(...result.items.map(renderMyCard))}
TypeScript12 lines

Behaviour worth knowing:

  • Never throws, never hangs. Unknown recipe, network failure, or timeout (3 seconds) all resolve to { items: [], served: false }. Check served and render nothing on a miss. degraded: true distinguishes "we could not ask" from an honest "no rows for this seed":

  • degraded also covers the build. Your site is served the smallest snippet that can do everything your published content needs, so a project with no shop features gets a build with no recommendations engine at all. If a page on that build calls get(), the result is degraded: true rather than a clean empty answer, because nothing was actually looked up. In normal use you will not see this: adding a recommendation to a project switches the shop features on at your next publish. It can appear briefly in the window where a visitor's browser still holds the older, smaller snippet, which clears itself once that cached copy refreshes.

TypeScript
function renderResult(result: RecsResult): void {  if (!result.served) {    // Two different reasons nothing came back: an honest miss (no rows for    // this seed) or an infrastructure failure. `degraded` tells them apart.    if (result.degraded) reportRecsOutage(result.recipe)    return  }  renderRecs(result.items)}
TypeScript9 lines
  • Seeds. Co-occurrence and similarity recipes want a seed product (context.seed, or up to 3 seeds on cart pages). List recipes (bestsellers, trending, new arrivals) need no context at all. context: { recentlyViewed: 3 } seeds from the visitor's on-device recently-viewed buffer.
  • Impressions are automatic. Every completed get() records one rec:impression (served or not), so exposure denominators line up. (An exposure is the moment a visitor is actually counted in an experiment, usually when they see the thing being tested.) Do not track impressions yourself.
  • Prices are minor units. price: 5999 with currency: 'USD' means $59.99. Format for display with Intl.NumberFormat:
TypeScript
function money(minor: number, currency: string): string {  const fmt = new Intl.NumberFormat(undefined, { style: 'currency', currency })  const digits = fmt.resolvedOptions().maximumFractionDigits ?? 2  return fmt.format(minor / Math.pow(10, digits))}
TypeScript5 lines
Out-of-stock items are already filtered

By default recipes drop out-of-stock products server-side before they reach you. A recipe (or a get() call) can opt out with excludeOutOfStock: false, in which case sold-out items arrive with availability: 'out_of_stock'.

Using recommendations inside an experiment: options.recs

To A/B test recommendations, each variation's code calls options.recs.get() with the recipe that variation should serve. A holdout is simply a variation whose code renders nothing.

TypeScript
// Variation A: serve the "similar products" recipeasync function initVariation(options: AvsbVariationOptions): Promise<void> {  const result = await options.recs.get({    recipe: 'similar-products',    context: { seed: getSeedSku() },  })  if (result.served) renderRecs(result.items)}
TypeScript8 lines

Variation B is the holdout. Its code renders nothing, and the exposure still counts:

TypeScript
// Variation B (holdout): render nothing; exposure still countsfunction initVariation(options: AvsbVariationOptions): void {}
TypeScript2 lines

Because options.recs is bound to the running variation, the results page groups rec:impression / rec:click by experiment, variation, and recipe with no extra wiring.

Click tracking

Clicks are not recorded automatically from arbitrary markup: the API does not own your DOM. Two options, pick one:

Convention (recommended): stamp each rendered card with data-avsb-rec (and optionally data-avsb-rec-pos). One shared listener records rec:click; it works with any framework and survives re-renders:

HTML
<a href="/products/knit-throw" data-avsb-rec="SKU-123" data-avsb-rec-pos="2">…</a>
HTML1 line

Explicit: call trackClick from your own click handler:

TypeScript
// One listener per card, attached as you render itresult.items.forEach((item, index) => {  const card = renderMyCard(item)  card.addEventListener('click', () => {    options.recs.trackClick(item.id, { position: index + 1 })  })})
TypeScript7 lines

trackClick flushes the event immediately (rec clicks usually navigate away).

More than one rec surface on a page

Sometimes a single page renders two or more recommendation surfaces from different get() calls: say a "Similar products" grid and a "Frequently bought together" strip, each its own experiment or variation. When that happens, tell the convention listener which surface a click belongs to. Give each get() a surface id, and wrap that surface's cards in a container carrying the matching data-avsb-rec-surface attribute:

TypeScript
// Surface 1: similar productsconst similar = await options.recs.get({  recipe: 'similar-products',  context: { seed: product.sku },  surface: 'similar',})// Surface 2: frequently bought togetherconst fbt = await options.recs.get({  recipe: 'bought-together',  context: { seed: product.sku },  surface: 'cart',})
TypeScript13 lines
HTML
<section data-avsb-rec-surface="similar">  <a data-avsb-rec="SKU-123" data-avsb-rec-pos="1">…</a></section><aside data-avsb-rec-surface="cart">  <a data-avsb-rec="SKU-987" data-avsb-rec-pos="1">…</a></aside>
HTML7 lines

A click walks up to its nearest data-avsb-rec-surface ancestor and is attributed to that surface's get(): its experiment, variation, and recipe. Cards with no data-avsb-rec-surface container fall back to the most recently served surface. Single-surface pages need neither the surface option nor the container attribute; attribution is already unambiguous.

avsb.recs.trackView

Plain text
avsb.recs.trackView(sku: string, meta?: { category?: string }) => void
Plain text1 line

Records that the visitor viewed a product. One call feeds two things:

  • The recently-viewed buffer behind context: { recentlyViewed: n }. The snippet keeps the visitor's last 20 viewed SKUs on the device (localStorage key avsb_rv), product IDs only, no personal data.
  • The viewed-product history behind the Products viewed audience condition. (An audience is a named, reusable group of visitors defined by targeting rules; see Commerce Conditions.) Pass meta.category if you target by category.

Behaviour worth knowing:

  • SKUs only, 256 characters max (categories too). Empty strings are ignored; re-viewing a product moves it to the front rather than duplicating it.
  • Safe to call early. Views recorded before the snippet initializes (or while Consent Mode is pending) are buffered in memory and flushed on init.
  • Never throws. If storage is unavailable (Safari private mode), the call is a silent no-op.

Event vocabulary: rec:impression and rec:click

The client records two events. They appear in exports and the events stream under these names, useful when you analyse raw data yourself. Results group both by (experiment, variation, recipe).

rec:impression

Fired once per completed get() call, served or not, so exposure denominators align.

AttributeMeaning
recipeThe recipe handle the call resolved
seedThe seed key(s) the lookup used, comma-joined (capped at 512 characters)
slugSlug of the chain step that actually served; empty when not served
versionVersion of the serving dataset; empty when not served
stepFallback chain step index that served (0 = primary); empty when not served
servedtrue or false: whether the chain had items for this seed
itemsComma-joined item ids in returned order (capped at 512 characters)

rec:click

Fired by trackClick or the data-avsb-rec convention listener.

AttributeMeaning
recipeThe recipe of the served get() for the clicked card's surface (see multi-surface)
itemThe clicked item's id
position1-based rendered position, when provided
Was this helpful?