Example: Calling the Recommendations API and tracking the displayed products
This example shows you how to request a recommendation from an API placement, render the returned products with your own markup, and add Frosmo tracking to the rendered products. Tracking records when a product is displayed, truly displayed (continuously visible for 3 seconds), and clicked, so that the recommendation shows up in reporting.
The example is a stand-alone script. It does not depend on any other documentation page.
Prerequisites
-
The Frosmo script (Frosmo Core) is on the page. The script provides the site origin, the visitor's Frosmo ID, and the
frosmo.easy.fetchfunction that the example uses. -
An API placement exists on the site. You create it in the Frosmo Control Panel. The placement's ID string is the value of
API_PLACEMENTin the example.
How the example works
The example has three parts:
-
Request. Builds the request for the API placement and fetches it with
frosmo.easy.fetch.get(). Use Frosmo Core's fetch function rather than the browser'sfetch(), because Frosmo Preview builds its API placements view (the request, the response, and the selected strategy) from the requests made through Frosmo Core. -
Render. Renders the returned products with your own markup. The only requirement is that each product element carries its product ID, for example in a
data-product-idattribute. The value must exactly match theidof the product in the response (the comparison is a string comparison, so01234and1234are different products). The example includes a minimal renderer for illustration. Replace it with your site's own rendering. -
Track. Passes the response and a description of the rendered markup (the container, a selector for the product elements, and how to read the product ID) to
addRecoTracking(). The function stamps tracking attributes onto the elements, sends the display, true display, and click events, and returns a function that removes the tracking.
The placement ID string is used twice in the example: to request the products and to name the source of the recommendation in tracking events. The two values must be identical. A mismatch splits one placement's data across two keys in reporting.
Page context
Some placements need to know which page the visitor is on, for example when a strategy uses the viewed product or the cart contents as an anchor or filter. If the request for such a placement does not include the page context, the API returns a 400 error. Set the CONTEXT constant in the example to the part of the context that the placement's strategies read. The product IDs and categories must be written the way the product feed stores them. The example shows the format for product, category, and cart pages.
For a placement that does not need the page context, such as a front page placement, leave CONTEXT undefined.
Example code
The example uses top-level await, so run it as a module script or inside an async function. The products must be in the document before addRecoTracking() is called. The function does not wait or retry.
/**
* Example: call the Frosmo recommendation API from your own site, render the products
* yourself, and add Frosmo tracking to what you rendered.
*
* Assumes the Frosmo script (Frosmo Core) is already on the page. Core supplies the two
* values the request needs - the site's origin and the visitor's cookie id - and the fetch
* function. The "addRecoTracking" function that attaches tracking comes from the site's shared
* code "[Helpers] API Recos - helpers", which is part of the site setup. This file writes no
* tracking calls of its own.
*
* Three parts:
*
* PART 1 Build the recoApi request URL for the API placement and fetch it.
* PART 2 Render the returned products with your own markup. (Not included - see the
* commented example markup. The only requirement tracking places on your markup is
* that each product element carries its product id.)
* PART 3 Hand the rendered markup to Frosmo so it tracks display / true display / click.
*
* WHAT YOU CHANGE for your site:
*
* API_PLACEMENT the api-placement id string from the Frosmo Control Panel
* CONTEXT the page the visitor is on, when the placement's strategies need it
*
*/
// The api-placement id string, created in the Frosmo Control Panel. The SERVER uses it to
// pick the recommendation strategy (targeting, schedule, sort order, comparison group).
//
// Used TWICE below and the two uses MUST agree: it selects the products in the request
// (PART 1) and names the source of the reco in every tracking event (PART 3). Reporting
// joins on it, so a mismatch silently splits one placement's data across two keys.
const API_PLACEMENT = 'front-page-reco';
// The page the visitor is on. Only needed when the placement's strategies read it (an anchor or
// a filter on page.product.id, page.category.categories, page.cart.ids ...); a request without
// it then fails with a 400 "error retrieving value from context". Leave undefined otherwise
// (a front page placement normally needs none). Send only the part the strategy reads: which
// one depends on the placement's strategies. The product ids and categories
// must be written the way the product feed stores them.
//
// Product page. "id" is the viewed product; "category" is its category path, needed by
// strategies that stay in, or exclude, the viewed product's category:
//
// const CONTEXT = { page: { product: { id: '01234', category: 'Home/Rugs' } } };
//
// Category page. "categories" is the category path of the page, parents first. A strategy
// matching with "starts with" returns products in that category and its sub-categories:
//
// const CONTEXT = { page: { category: { categories: ['Home/Rugs'] } } };
//
// Cart page. "ids" are the product ids in the cart, in order (up to 20 are used; earlier ids
// weigh more):
//
// const CONTEXT = { page: { cart: { ids: ['01234', '05678'] } } };
const CONTEXT = undefined;
// =========================================================================================
// PART 1 - Call the recoApi for the API placement
// =========================================================================================
const easy = window.frosmo.easy;
const params = {
method: 'placement',
origin: easy.api.getOrigin(),
cookieId: easy.uid.get(),
placement: API_PLACEMENT
};
if (CONTEXT) {
params.context = JSON.stringify(CONTEXT);
}
// Use Frosmo Core's fetch, not the browser's. Core records every easy.fetch call, and Frosmo
// Preview builds its API placements page (the request, the response, the selected strategy)
// from those records; a request made with the browser's fetch is invisible to it. The query
// string is built from "params"; the response is parsed JSON. A failed request rejects with
// { xhr, textStatus, errorThrown }.
const res = await easy.fetch.get(easy.api.getApiUrl('reco') + '/v2', { params: params });
const products = res.data || [];
// =========================================================================================
// PART 2 - Render the products with your own markup (NOT INCLUDED)
// =========================================================================================
// Render the products however your site renders products - your template engine, React, Vue,
// plain DOM. There is nothing Frosmo-specific to put in the markup.
//
// The ONE thing tracking needs from your markup: each product element must carry its product
// id, so Frosmo can match your elements to the response's products. A data attribute is
// simplest; the value must equal "product.id" from the response EXACTLY (string compare -
// "01234" and "1234" are different products).
//
// What "render" below has to produce inside the container - one tile per product:
//
// <div id="frosmo-api-reco">
// <a class="my-product-card" href="/p/01234" data-product-id="01234">
// <img src="https://cdn.example/01234.jpg" alt="Product name">
// <div>Product name</div>
// </a>
// <a class="my-product-card" href="/p/05678" data-product-id="05678">
// <img src="https://cdn.example/05678.jpg" alt="Another product">
// <div>Another product</div>
// </a>
// ...
// </div>
//
// - "my-product-card" is on the OUTER tile - visibility is measured from it, so give it a
// height that does not depend on the <img> having loaded (a zero-height tile reads as
// "not visible" and is never truly displayed).
// - "data-product-id" is what Frosmo matches on. It must equal "product.id" from the
// response exactly.
// - Escape catalog values (name, url, image, id) before interpolating them into HTML.
// - Replace, don't append, on a re-run, so a second render does not stack two sets of tiles.
const container = document.querySelector('#frosmo-api-reco');
// Minimal example renderer. Replace it with your site's own rendering.
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, function (c) {
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
});
}
function render(target, items) {
target.innerHTML = items.map(function (p) {
return '<a class="my-product-card" href="' + escapeHtml(p.url) + '" data-product-id="' + escapeHtml(p.id) + '">' +
'<img src="' + escapeHtml(p.image) + '" alt="' + escapeHtml(p.name) + '">' +
'<div>' + escapeHtml(p.name) + '</div>' +
'</a>';
}).join('');
}
render(container, products);
// =========================================================================================
// PART 3 - Add Frosmo tracking to the rendered markup
// =========================================================================================
// Hand the container to Frosmo Core. From here Frosmo:
// - stamps data-frosmo-reco-* attributes onto your elements, from the response
// - fires a "display" per product immediately
// - fires a "trueDisplay" once a product has been continuously visible for 3 seconds
// - fires a "click" when a product is clicked
// - unbinds everything on teardown
//
// You write NO tracking calls. You describe the rendered result with CSS selectors.
//
// The products must be IN THE DOCUMENT before this runs (visibility is read from
// getBoundingClientRect; a detached or unlaid-out element reads as not visible).
// addRecoTracking does not wait, poll or retry - whatever is in the DOM at the moment of the
// call is what gets tracked.
//
// "res" is passed straight through, unmodified: addRecoTracking reads recoRequestId /
// placementId / recoVariant / data off it directly. Render every product in res.data. If you
// render fewer, pass { ...res, data: <the rendered products> }, or the others are logged as an
// error ("matched 8 of 21 products").
const teardown = window.frosmo.site.recommendations.addRecoTracking({
// The response, so Frosmo can read recoRequestId / placementId / recoVariant / data.
res: res,
// What served this reco. The placement id string - identical to API_PLACEMENT above. The
// numeric id that goes on the wire is taken from res.placementId.
apiPlacement: API_PLACEMENT,
// The mount point you rendered into - the element that was already in the page, not a
// wrapper your renderer created. Frosmo scopes everything below to it.
container: container,
// Selects each product tile within the container. Resolved inside it, so the class need not
// be unique on the page. Must select the OUTER tile - visibility is measured from it.
productSelector: '.my-product-card',
// How to read each product's id off its tile. A STRING is an attribute name
// (el.getAttribute(productId)); a FUNCTION "(el) => id" covers an id that lives in an href
// or child text. NOT a selector. The value must equal res product ids exactly.
productId: 'data-product-id'
// productsEl: <the inner strip>
// OPTIONAL. Only if this widget is a carousel AND the element that scrolls horizontally
// is INSIDE the container rather than the container itself. Without it, a visitor who
// pages the carousel and pauses briefly can page again before the next once-per-second
// visibility check, and the products seen in between are never credited. Omit it for a
// static grid.
});
// Release the dwell timers and click handlers when the page goes away.
window.addEventListener('pagehide', function () {
teardown();
});