Tracking search analytics events in headless web stores
Learn how to add data-af-* tracking attributes to capture Intelligent Search analytics events in your headless web store using Activity Flow.
In this guide, you will learn how to add tracking attributes to search screens in a headless web store so that Activity Flow can capture Intelligent Search analytics events: impressions and clicks on search results and autocomplete suggestions.
The Activity Flow web script reads data-af-* attributes directly from the DOM. You don't need to write a listener or call any function: render the attributes on the right element, and the script takes care of the rest. The data-af-element attribute defines the elementSource, sent at the top level of the event payload, while any other data-af-<name> attribute becomes an extra parameter inside the attributes object.
Before you begin
This guide assumes Activity Flow is already installed in your headless web store. If you haven't set it up yet, see Installing Activity Flow in headless stores.
Understanding the tracking attributes
Trigger attributes define which event fires:
| Attribute | Function |
|---|---|
data-af-element | Required. Identifies the origin of the event. Without it, impressions and clicks are discarded by the script. |
data-af-onimpression | Fires an impression when the element is rendered, and again when it's re-rendered with different parameters. |
data-af-onclick | Fires a click when the element or any of its descendants is clicked. |
data-af-element must be one of the following values:
| Value | Screen |
|---|---|
search-result | Search results gallery, including the no-results search screen |
search-autocomplete | Autocomplete product suggestions |
Extra parameters:
| Attribute | Required | Description |
|---|---|---|
data-af-search-id | Yes, in every event | The search identifier, returned by the Intelligent Search response itself. |
data-af-product-id | Yes, on click | The ID of the clicked product. |
data-af-product-position | Yes, on click | The product's position in the list, 1-based, absolute across pages. |
data-af-product-specification | No | The product's specification value, when the API returns it. |
searchIdis generated by Intelligent Search and returned in the search response (productSearch.searchIdon the results gallery,productSuggestions.searchIdon autocomplete). Never create, derive, or reuse this value.
Tracking the results gallery
Follow the steps below to track impressions and clicks on the search results gallery, including the no-results screen.
-
Compute the tracking gate from the search response. A search that resulted in a redirect never shows a results gallery, so it must not render any attributes:
_10const { searchId, redirect } = searchQuery?.data?.productSearch || {}_10_10const shouldAddAFAttr = searchId && !redirectIf
searchIddidn't come back in the response or is empty, don't render the attributes. Passingundefinedto all of them, as shown throughout this guide, omits them from the HTML instead of rendering them with an empty value. It's better to lose the event than to record one that can't be attributed to a search. -
Render the following attributes on the gallery container to fire the impression. Use the same container and attributes on the no-results screen, since a search without results still counts as exactly one impression. In that case, the element carries only these trigger attributes and
search-id, since there's no product to report:_10<div_10data-af-element={shouldAddAFAttr ? 'search-result' : undefined}_10data-af-onimpression={shouldAddAFAttr ? true : undefined}_10data-af-search-id={shouldAddAFAttr ? searchId : undefined}_10className={/* ... */}_10>_10{children}_10</div>Each new query, such as applying a filter, changing the sort order, or paging, produces a new
searchId. Since the script re-fires the impression whenever the attributes change, rendering the newsearchIdautomatically triggers a new impression. Don't group interactions from different queries under the samesearchId. -
Render the following attributes on each gallery item's wrapper to fire the click. Note that the item's gate also requires
productId:_19const absoluteProductIndex =_19productsPositionOffset + rowIndex * itemsPerRow + index + 1_19_19const shouldAddAFAttr = searchId && !redirect && product.productId_19_19return (_19<div_19data-af-element={shouldAddAFAttr ? 'search-result' : undefined}_19data-af-onclick={shouldAddAFAttr ? true : undefined}_19data-af-search-id={shouldAddAFAttr ? searchId : undefined}_19data-af-product-position={shouldAddAFAttr ? absoluteProductIndex : undefined}_19data-af-product-id={shouldAddAFAttr ? product.productId : undefined}_19data-af-product-specification={_19shouldAddAFAttr ? product.specification : undefined_19}_19>_19<GalleryItem item={product} position={absoluteProductIndex} />_19</div>_19)Click attributes go on the wrapper that contains the clickable area. The script looks for the closest ancestor with
data-af-onclick, so you only need to add the attributes to the element that wraps the product card, without touching internal components. This also works when an internal component stops click propagation.productsPositionOffsetis what keeps the position absolute across pages: it represents how many products came before the current page, accounting for pagination, infinite scroll, and direct loads via?page=N. Without it, the first product on any page would be reported as1, and position-based relevance metrics would be skewed.
Tracking autocomplete
Autocomplete follows the same pattern as the results gallery, but with its own data-af-element value and no pagination.
-
Render the following attributes on the suggestions list container to fire the impression:
_19<section_19data-af-element={searchId ? 'search-autocomplete' : undefined}_19data-af-onimpression={searchId ? true : undefined}_19data-af-search-id={searchId}_19>_19{products.map((product, index) => (_19<li_19key={product.productId}_19data-af-element={searchId ? 'search-autocomplete' : undefined}_19data-af-onclick={searchId && product.productId ? true : undefined}_19data-af-search-id={searchId}_19data-af-product-position={index + 1}_19data-af-product-id={product.productId}_19data-af-product-specification={product.specification}_19>_19{/* product card */}_19</li>_19))}_19</section>The position is simply
index + 1, since autocomplete has no pagination.Autocomplete and the results gallery are distinct searches. Don't propagate the suggestions'
searchIdto the results screen when the shopper submits the search: that screen has its ownsearchId.
Checking the resulting payload
A click on a gallery item generates a payload similar to the following:
_10{_10 "eventType": "click",_10 "element": "div",_10 "elementSource": "search-result",_10 "attributes": {_10 "data-af-search-id": "b3f1c9d2-...",_10 "data-af-product-id": "309",_10 "data-af-product-position": "3"_10 }_10}
The remaining fields (session, account, URL, referrer, timestamp) are filled in automatically by the script. Unlike mobile apps, on the storefront the keys inside attributes preserve the full HTML attribute name, with the data-af- prefix.
Checklist
- Impression rendered on every search, including the no-results layout.
data-af-elementpresent on every instrumented element, with the valuesearch-resultorsearch-autocomplete.data-af-search-idcoming from the API and present on both the impression and the click.data-af-product-positionis 1-based and correct after pagination or infinite scroll.- No attributes rendered on a search that redirects or has no
searchId.