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 sameget,trackClick, andtrackViewmethods aswindow.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.
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'}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:
declare function renderMyCard(item: RecProduct): HTMLElementdeclare function renderRecs(items: RecProduct[]): voiddeclare function getSeedSku(): stringdeclare function reportRecsOutage(recipe: string): voidavsb.recs.get
avsb.recs.get(req: RecsRequest) => Promise<RecsResult>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.
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))}Behaviour worth knowing:
-
Never throws, never hangs. Unknown recipe, network failure, or timeout (3 seconds) all resolve to
{ items: [], served: false }. Checkservedand render nothing on a miss.degraded: truedistinguishes "we could not ask" from an honest "no rows for this seed": -
degradedalso 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 callsget(), the result isdegraded: truerather 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.
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)}- Seeds. Co-occurrence and similarity recipes want a seed product (
context.seed, or up to 3seedson 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 onerec: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: 5999withcurrency: 'USD'means $59.99. Format for display withIntl.NumberFormat:
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))}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.
// 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)}Variation B is the holdout. Its code renders nothing, and the exposure still counts:
// Variation B (holdout): render nothing; exposure still countsfunction initVariation(options: AvsbVariationOptions): void {}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:
<a href="/products/knit-throw" data-avsb-rec="SKU-123" data-avsb-rec-pos="2">…</a>Explicit: call trackClick from your own click handler:
// 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 }) })})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:
// 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',})<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>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
avsb.recs.trackView(sku: string, meta?: { category?: string }) => voidRecords 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 (localStoragekeyavsb_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.categoryif 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.
| Attribute | Meaning |
|---|---|
recipe | The recipe handle the call resolved |
seed | The seed key(s) the lookup used, comma-joined (capped at 512 characters) |
slug | Slug of the chain step that actually served; empty when not served |
version | Version of the serving dataset; empty when not served |
step | Fallback chain step index that served (0 = primary); empty when not served |
served | true or false: whether the chain had items for this seed |
items | Comma-joined item ids in returned order (capped at 512 characters) |
rec:click
Fired by trackClick or the data-avsb-rec convention listener.
| Attribute | Meaning |
|---|---|
recipe | The recipe of the served get() for the clicked card's surface (see multi-surface) |
item | The clicked item's id |
position | 1-based rendered position, when provided |